如何使用 Swift 开发简单的条形码检测器?

Chl54T 8年前
   <p>【编者按】本文作者为 Matthew Maher,主要手把手地介绍如何用Swift 构建简单的条形码检测器。文章系 <a href="/misc/goto?guid=4958877054569471341" rel="nofollow,noindex"> <strong>OneAPM</strong> </a> <strong>工程师编译整理。</strong></p>    <p>超市收银员对货物进行扫码,机场内录入行李或检查乘客,或是在大型零售商的存货管理等活动中,条形码扫码器都是一个简单而实用的工具。事实上,条形码扫码器还帮助消费者实现了智能购物,货物分类等用途。这次,我们将为iPhone开发一个扫码器。</p>    <p>我们很幸运,苹果公司让条形码扫描过程的实现变得很简单。我们将会深入AV Foundation框架开发一个简单的能够扫描CD条形码的app,然后获得专辑的关键信息,最后在app的界面中打印出来。阅读条形码很酷炫也很重要,我们会根据读到的条形码采取进一步的操作。</p>    <p>不用多说,能扫码的设备必须要有一个摄像头。从这里开始,让我们拿一个配备有摄像头的iOS设备开始干活吧!</p>    <h2>简介 CDbarcodes</h2>    <p>我们今天开发的这个app名叫CDBarcodes——通俗易懂,即条形码扫描对象是CD。当我们的设备检测到一个条形码时,会拾取这个货码然后发送到Discogs的数据库,获得其专辑名称、艺人姓名以及发布年份。Discogs的音乐数据库十分强大,因此我们很有可能找到一些实用信息。</p>    <p>下载CDBarcodes的 <a href="/misc/goto?guid=4959674537259633688" rel="nofollow,noindex">初始项目</a> 。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/380672eb9e38d4bf690303be8676c458.png"></p>    <p>除了一个不错的数据库,Discogs还有一个实用的API来帮助查询。我们涉及的仅仅是Discogs提供给开发者的一小部分功能,不过这已经足够使我们的app跑起来了。</p>    <h2>Discogs</h2>    <p>进入 <a href="/misc/goto?guid=4959674537348062356" rel="nofollow,noindex">Discogs</a> 网站。首先我们必须注册一个Discogs账号并登录。在这之后,下拉到页面最底端。在页尾最左栏点击API。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/560b01a38f33df9f9717ca4e866425c2.png"></p>    <p>在Discogs的API界面左侧的数据库区域点击搜索(Search)。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/813b4dcba4d2157bb82b1dada8ddc326.png"></p>    <p>这是我们查询的端点。我们将会从“title”和“year”这两个参数上获得专辑信息。</p>    <p>现在,我们将这个URL记录在CDBarcodes中以便后面的查询。在 Constants.swift 中添加 DISCOGS_AUTH_URL 并赋值 https://api.discogs.com/database/search?q= 作为常量。</p>    <pre>  <code class="language-swift">let DISCOGS_KEY = "your-discogs-key"</code></pre>    <p>现在我们能够在整个app里面通过 DISCOGS_AUTH_URL 调用URL。</p>    <p>回到Discogs的API页面,选择创建一个新的app,并获得一些认证信息。在页面顶端的导航栏中,找到“Create an App”,点击该按钮。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/380672eb9e38d4bf690303be8676c458.png"></p>    <p>在应用名称栏里输入“CDBarcodes Your Name”,或是其他合适的名字。描述可以使用下面的文字:</p>    <p>“这是一个iOS应用,旨在在读取CD的条形码后显示专辑信息。”</p>    <p>然后,点击“Create Application”(即创建应用)按钮。</p>    <p>在结束页面,会看到允许我们使用条形码的认证信息。</p>    <p>复制“Consumer Key”(用户秘钥)到 Constants.swift 的 DISCOGS_KEY 里面。</p>    <p>有了这个URL,我们可以很方便的在整个CDBarcodes应用里使用这些参数。</p>    <p><img src="https://simg.open-open.com/show/87fb807214885244f72629ae1e09fcf4.png"></p>    <h2>CocoaPods</h2>    <p>我们使用功能强大的依赖管理器(dependency manager)CocoaPods来与Discogs的API进行交互。有关CocoaPods的安装和其他信息,可以参照 <a href="/misc/goto?guid=4958876601167114464" rel="nofollow,noindex">CocoaPods官网</a> 。</p>    <p>经由CocoaPods,在网络端我们将会使用Alamofire,并借助SwiftyJSON来处理Discogs返回的JSON。</p>    <p>现在开始在CDBarcodes实战吧!</p>    <p>安装好CocoaPods,打开终端界面,调至CDBarcodes,在Xcode项目中使用下面的代码初始化CoccoaPods:</p>    <pre>  <code class="language-swift">cd <your-xcode-project-directory>  pod init</code></pre>    <p>在Xcode里打开Podfile文件:</p>    <pre>  <code class="language-swift">open -a Xcode Podfile</code></pre>    <p>输入或是复制粘贴下面的代码至Podfile文件:</p>    <pre>  <code class="language-swift">source 'https://github.com/CocoaPods/Specs.git'  platform :ios, '8.0'  use_frameworks!    pod 'Alamofire', '~> 3.0'    target ‘CDBarcodes’ do  pod 'SwiftyJSON', :git => 'https://github.com/SwiftyJSON/SwiftyJSON.git'  end</code></pre>    <p>最后,运行下面的代码下载Alamofire和SwiftyJSON:</p>    <pre>  <code class="language-swift">pod install</code></pre>    <p>现在回到Xcode!注意开发app时要保持打开CDBarcodes.xcworkspace(工作区)。</p>    <h2>条形码阅读器</h2>    <p>苹果的AV Foundation框架提供了我们开发这个条形码阅读器app需要的相关工具。下面是整个过程中会涉及到的几个方面:</p>    <ul>     <li> <p>AVCaptureSession将会处理来自相机的输入输出数据。</p> </li>     <li> <p>AVCaptureDevice指的是物理设备及其它的属性。AVCaptureSession从AVCaptureDevice这里接受输入信息。</p> </li>     <li> <p>AVCaptureDeviceInput从输入设备获取输入数据。</p> </li>     <li> <p>AVCaptureMetadataOutput将元数据对象发送至代理对象(delegate object)处进行处理。</p> </li>    </ul>    <p>在 BarcodeReaderViewController.swift 里面,我们的第一步操作是导入AVFoundation。</p>    <pre>  <code class="language-swift">import UIKit  import AVFoundation</code></pre>    <p>注意要遵循 AVCaptureMetadataOutputObjectsDelegate 。</p>    <p>在 viewDidLoad() ,将运行我们的条形码阅读引擎。</p>    <p>首先,新建一个 AVCaptureSession 对象并设置 AVCaptureDevice 。然后,我们新建一个输入对象并添加至 AVCaptureSession 。</p>    <pre>  <code class="language-swift">class BarcodeReaderViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {    var session: AVCaptureSession!  var previewLayer: AVCaptureVideoPreviewLayer!    override func viewDidLoad() {      super.viewDidLoad()        // Create a session object. 新建一个模块对象      session = AVCaptureSession()        // Set the captureDevice. 设置captureDevice      let videoCaptureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)        // Create input object. 新建输入设备      let videoInput: AVCaptureDeviceInput?        do {          videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)      } catch {          return      }        // Add input to the session. 将输入添加至模块中      if (session.canAddInput(videoInput)) {          session.addInput(videoInput)      } else {          scanningNotPossible()      }</code></pre>    <p>如果设备碰巧没有摄像头时,扫描过程将不可能实现。因此,我们需要一个报错函数。在这里,我们通知用户寻找一个有相机的iOS设备以便进行下一步CD条形码的读取。</p>    <pre>  <code class="language-swift">func scanningNotPossible() {      // Let the user know that scanning isn't possible with the current device. 告知用户扫描现有设备无法扫描      let alert = UIAlertController(title: "Can't Scan.", message: "Let's try a device equipped with a camera.", preferredStyle: .Alert)      alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))      presentViewController(alert, animated: true, completion: nil)      session = nil  }</code></pre>    <p>回到 viewDidLoad() ,在将输入添加至(session)模块后,我们接着新建 AVCaptureMetadataOutput 并将它添加到模块中。我们将捕捉到的数据通过一个串行序列的形式发送给代理对象。</p>    <p>下一步就是明确我们应该扫描的条形码类型。在这里我们面对的是EAN-13类型的条形码。有趣的是,并不是所有的条形码都是这种类型;有一些将会是UPC-A格式。这可能会导致错误出现。</p>    <p>苹果会自动将UPC-A格式的条形码前面加一个0后转为EAN-13格式。UPC-A格式的条形码仅仅有12位数字;而在EAN-13格式的条形码中则是13位。这个自动转换过程的一个好处是我们可以查询 metadataObjectTypes AVMetadataObjectTypeEAN13Code ,因此两种格式的条形码我们就都能读取了。需要注意的是这个转换会直接改变条形码从而误导Discogs数据库。不过不用担心,我们马上就会解决这个问题。</p>    <p>无论如何,在用户设备相机有问题时我们就将用户引导至 scanningNotPossible() 函数。</p>    <pre>  <code class="language-swift">// Create output object. 新建输出对象  let metadataOutput = AVCaptureMetadataOutput()    // Add output to the session. 将输出添加至模块  if (session.canAddOutput(metadataOutput)) {      session.addOutput(metadataOutput)        // Send captured data to the delegate object via a serial queue. 通过串行序列将捕捉到的数据发送至代理对象。      metadataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue())        // Set barcode type for which to scan: EAN-13. 设置需要扫描的条形码类型:EAN-13      metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeEAN13Code]    } else {      scanningNotPossible()  }</code></pre>    <p>现在我们就搞定了这个酷炫的功能,拉出来溜溜吧!我们将使用 AVCaptureVideoPreviewLayer 以整个屏幕展示视频。</p>    <p>最后,我们开始捕捉模块。</p>    <pre>  <code class="language-swift">// Add previewLayer and have it show the video data. 添加previewLayer并展示视频数据        previewLayer = AVCaptureVideoPreviewLayer(session: session);      previewLayer.frame = view.layer.bounds;      previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;      view.layer.addSublayer(previewLayer);        // Begin the capture session. 开启捕捉模块        session.startRunning()</code></pre>    <p>In captureOutput:didOutputMetadataObjects:fromConnection , we celebrate, as our barcode reader found something!</p>    <p>通过 captureOutput:didOutputMetadataObjects:fromConnection ,我们的条形码阅读器终于读取到了一些数据。</p>    <p>首先,我们需要使用第一个对象获得 metadataObjects 数组并将其转换为可机读代码。然后,我们将 readableCode 字符串发送至 barcodeDetected() 。</p>    <p>在进入 barcodeDetected() 函数前,我们会停止捕捉模块并给用户一个震动反馈。如果我们忘了叫停捕捉模块,那么震动也就停不下来了!这也是为什么这是一个好案例的原因。</p>    <pre>  <code class="language-swift">func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {        // Get the first object from the metadataObjects array. 获得metadataObjects数组的第一个对象      if let barcodeData = metadataObjects.first {          // Turn it into machine readable code 转换为可机读代码          let barcodeReadable = barcodeData as? AVMetadataMachineReadableCodeObject;          if let readableCode = barcodeReadable {              // Send the barcode as a string to barcodeDetected() 发送条形码数据              barcodeDetected(readableCode.stringValue);          }            // Vibrate the device to give the user some feedback. 震动反馈          AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))            // Avoid a very buzzy device. 结束捕捉模块          session.stopRunning()      }  }</code></pre>    <p>在 barcodeDetected() 函数里面我们有很多事情要做。第一个任务是在震动反馈之后,提示用户我们已经发现了条形码。然后我们利用找到的数据开始干活!</p>    <p>条形代码中的空格必须移除。在这之后我们需要确认条形码格式是EAN-13还是UPC-A。如果是EAN-13我们可以直接使用。如果对象是一个UPC-A代码,那么它已经被转化为EAN-13格式,我们需要将其转换为原始格式。</p>    <p>如我们前文已经讨论的那样,苹果设备在UPC-A格式的条形码前添加一个0将其转化为EAN-13格式,因此我们首先确定代码是以0开头的。如果是,我们需要将它移除。少了这一步,Discogs数据库将不能识别这个数字,我们也就得不到想要的数据了。</p>    <p>在获得清理后的条形码字符串后,我们将它发送至 DataService.searchAPI() 并弹出 BarcodeReaderViewController.swift 。</p>    <pre>  <code class="language-swift">func barcodeDetected(code: String) {        // Let the user know we've found something. 告知用户扫描结果      let alert = UIAlertController(title: "Found a Barcode!", message: code, preferredStyle: UIAlertControllerStyle.Alert)      alert.addAction(UIAlertAction(title: "Search", style: UIAlertActionStyle.Destructive, handler: { action in            // Remove the spaces. 移除空格          let trimmedCode = code.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())            // EAN or UPC?  确定格式          // Check for added "0" at beginning of code.            let trimmedCodeString = "\(trimmedCode)"          var trimmedCodeNoZero: String            if trimmedCodeString.hasPrefix("0") && trimmedCodeString.characters.count > 1 {              trimmedCodeNoZero = String(trimmedCodeString.characters.dropFirst())                // Send the doctored UPC to DataService.searchAPI() 将UPC发送至API              DataService.searchAPI(trimmedCodeNoZero)          } else {                // Send the doctored EAN to DataService.searchAPI()              DataService.searchAPI(trimmedCodeString)          }            self.navigationController?.popViewControllerAnimated(true)      }))        self.presentViewController(alert, animated: true, completion: nil)  }</code></pre>    <p>在离开 BarcodeReaderViewController.swift 之前,在 viewDidLoad() 下面,我们添加 viewWillAppear() 和 viewWillDisappear() 函数。 viewWillAppear() 将会开启捕捉模块;而 viewWillDisappear() 会终止这一模块。</p>    <pre>  <code class="language-swift">override func viewWillAppear(animated: Bool) {        super.viewWillAppear(animated)      if (session?.running == false) {          session.startRunning()      }  }    override func viewWillDisappear(animated: Bool) {      super.viewWillDisappear(animated)        if (session?.running == true) {          session.stopRunning()      }  }</code></pre>    <h2>数据服务</h2>    <p>在 DataService.swift 里,我们首先要导入Alamofire 和 SwiftyJSON。</p>    <p>接着,我们声明一些变量以便存储从Discogs返回的原始数据。根据Bionik6的建议,我们巧妙地使用 private(set) 函数避免了用户可能导致的阻塞问题。</p>    <p>然后,建立Alamofire GET请求。在这里JSON会被解析,从而获得专辑的title(名称)和year(发行年份)。将原始的title和year字符串赋给 ALBUM_FROM_DISCOGS 和 YEAR_FROM_DISCOGS ,在后文将会用到它们来初始化我们的专辑。</p>    <p>现在,我们拥有了来自Discogs的数据,我们可以正式开秀了;随之我们通知 AlbumDetailsViewController.swift 模块捕捉到的信息。</p>    <pre>  <code class="language-swift">import Foundation  import Alamofire  import SwiftyJSON    class DataService {    static let dataService = DataService()    private(set) var ALBUM_FROM_DISCOGS = ""  private(set) var YEAR_FROM_DISCOGS = ""    static func searchAPI(codeNumber: String) {      // The URL we will use to get out album data from Discogs 使用URL获得数据      let discogsURL = "\(DISCOGS_AUTH_URL)\(codeNumber)&?barcode&key=\(DISCOGS_KEY)&secret=\(DISCOGS_SECRET)"        Alamofire.request(.GET, discogsURL)          .responseJSON { response in                var json = JSON(response.result.value!)                let albumArtistTitle = "\(json["results"][0]["title"])"              let albumYear = "\(json["results"][0]["year"])"                self.dataService.ALBUM_FROM_DISCOGS = albumArtistTitle              self.dataService.YEAR_FROM_DISCOGS = albumYear                // Post a notification to let AlbumDetailsViewController know we have some data. 通知AlbumDetailsViewController              NSNotificationCenter.defaultCenter().postNotificationName("AlbumNotification", object: nil)      }  }    }</code></pre>    <h2>专辑模块</h2>    <p>在专辑模块 Album.swift 中,我们会处理专辑数据以便符合我们的要求。这个模块将会获取原始的 artistAlbum 和 albumYear 字符串然后将它们用户友好化。在 AlbumDetailsViewController.swift 我们展示加工后的 album 和 year 信息。</p>    <pre>  <code class="language-swift">import Foundation    class Album {            private(set) var album: String!  private(set) var year: String!    init(artistAlbum: String, albumYear: String) {        // Add a little extra text to the album information 添加额外专辑信息      self.album = "Album: \n\(artistAlbum)"      self.year = "Released in: \(albumYear)"  }    }</code></pre>    <h2>专辑展示时间!</h2>    <p>在 viewDidLoad() 模块中,设置好指向条形码阅读器的标签(label)。然后,我们需要在 NSNotification 添加观察者(Observer),以便我们已经展示的提示能够集群。在 deinit 中,我们会移除观察者(Observer)。</p>    <pre>  <code class="language-swift">deinit {      NSNotificationCenter.defaultCenter().removeObserver(self)  }    override func viewDidLoad() {      super.viewDidLoad()        artistAlbumLabel.text = "Let's scan an album!"      yearLabel.text = ""        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(setLabels(_:)), name: "AlbumNotification", object: nil)  }</code></pre>    <p>当通知出现时, setLabels() 函数将会被调用。在这里,我们会使用来自 DataService.swift 的原始数据初始化 Album 。标签将会展示加工后的字符串。</p>    <pre>  <code class="language-swift">func setLabels(notification: NSNotification){        // Use the data from DataService.swift to initialize the Album.      let albumInfo = Album(artistAlbum: DataService.dataService.ALBUM_FROM_DISCOGS, albumYear: DataService.dataService.YEAR_FROM_DISCOGS)      artistAlbumLabel.text = "\(albumInfo.album)"      yearLabel.text = "\(albumInfo.year)"  }</code></pre>    <h2>测试 CDBarcodes</h2>    <p>应用搭建完毕,扫一下CD的条形码我们就能确定专辑的名称,艺人和发行年份信息,这很有意思!为了更好的测试CDBarcodes,我们可以随机找一些CD或是黑胶唱片。这样我们就更有机会同时遇到EAN-13和UPC-A两种条形码格式的案例。目前我们两者都能处理!</p>    <p>为了使应用顺利运行至BarcodeReaderViewController模块,注意避免闪光以确保相机能捕捉到条形码信息。</p>    <p>这里是完整代码的 <a href="/misc/goto?guid=4959674537458362168" rel="nofollow,noindex">下载链接</a> 。</p>    <h2>结论</h2>    <p>不管是商人,机智的消费者还是一般人士,这个条形码阅读器都很实用。因此,开发者拿这个案例来练练手是极好的。</p>    <p>但是我们也看到有趣的仅仅是扫码部分。在获得数据后,我们遇到了一点小问题,如EAN-13 和 UPC-A格式问题。我们找到了解决问题应对需求的办法。</p>    <p>接下来,我们可以探讨一些其他的 metadataObjectTypes 以及一些新API。机会无穷,经验无价。</p>    <p>本文系OneAPM 工程师编译整理。 <a href="/misc/goto?guid=4959674537545932051" rel="nofollow,noindex">OneAPM Mobile Insight</a> 以真实用户体验为度量标准进行Crash 分析,监控网络请求及网络错误,提升用户留存。访问OneAPM 官方网站感受更多应用性能优化体验,想阅读更多技术文章,请访问OneAPM 官方技术博客。</p>    <p>来自: <a href="/misc/goto?guid=4959674537625118387" rel="nofollow">http://blog.oneapm.com/apm-tech/758.html</a></p>    <p> </p>