【编者按】本文做者为 Matthew Maher,主要手把手地介绍如何用 Swift 构建简单的条形码检测器。文章系 OneAPM 工程师编译整理。html
超市收银员对货物进行扫码,机场内录入行李或检查乘客,或是在大型零售商的存货管理等活动中,条形码扫码器都是一个简单而实用的工具。事实上,条形码扫码器还帮助消费者实现了智能购物,货物分类等用途。此次,咱们将为iPhone开发一个扫码器。ios
咱们很幸运,苹果公司让条形码扫描过程的实现变得很简单。咱们将会深刻AV Foundation框架开发一个简单的可以扫描CD条形码的app,而后得到专辑的关键信息,最后在app的界面中打印出来。阅读条形码很酷炫也很重要,咱们会根据读到的条形码采起进一步的操做。git
不用多说,能扫码的设备必需要有一个摄像头。从这里开始,让咱们拿一个配备有摄像头的iOS设备开始干活吧!github
咱们今天开发的这个app名叫CDBarcodes——通俗易懂,即条形码扫描对象是CD。当咱们的设备检测到一个条形码时,会拾取这个货码而后发送到Discogs的数据库,得到其专辑名称、艺人姓名以及发布年份。Discogs的音乐数据库十分强大,所以咱们颇有可能找到一些实用信息。数据库
下载CDBarcodes的初始项目。json
除了一个不错的数据库,Discogs还有一个实用的API来帮助查询。咱们涉及的仅仅是Discogs提供给开发者的一小部分功能,不过这已经足够使咱们的app跑起来了。swift
进入Discogs网站。首先咱们必须注册一个Discogs帐号并登陆。在这以后,下拉到页面最底端。在页尾最左栏点击API。api
在Discogs的API界面左侧的数据库区域点击搜索(Search)。数组
这是咱们查询的端点。咱们将会从“title”和“year”这两个参数上得到专辑信息。xcode
如今,咱们将这个URL记录在CDBarcodes中以便后面的查询。在Constants.swift
中添加DISCOGS_AUTH_URL
并赋值https://api.discogs.com/database/search?q=
做为常量。
let DISCOGS_KEY = "your-discogs-key"
如今咱们可以在整个app里面经过DISCOGS_AUTH_URL
调用URL。
回到Discogs的API页面,选择建立一个新的app,并得到一些认证信息。在页面顶端的导航栏中,找到“Create an App”,点击该按钮。
在应用名称栏里输入“CDBarcodes Your Name”,或是其余合适的名字。描述可使用下面的文字:
“这是一个iOS应用,旨在在读取CD的条形码后显示专辑信息。”
而后,点击“Create Application”(即建立应用)按钮。
在结束页面,会看到容许咱们使用条形码的认证信息。
复制“Consumer Key”(用户秘钥)到Constants.swift
的DISCOGS_KEY
里面。
有了这个URL,咱们能够很方便的在整个CDBarcodes应用里使用这些参数。
咱们使用功能强大的依赖管理器(dependency manager)CocoaPods来与Discogs的API进行交互。有关CocoaPods的安装和其余信息,能够参照CocoaPods官网。
经由CocoaPods,在网络端咱们将会使用Alamofire,并借助SwiftyJSON来处理Discogs返回的JSON。
如今开始在CDBarcodes实战吧!
安装好CocoaPods,打开终端界面,调至CDBarcodes,在Xcode项目中使用下面的代码初始化CoccoaPods:
cd <your-xcode-project-directory> pod init
在Xcode里打开Podfile文件:
open -a Xcode Podfile
输入或是复制粘贴下面的代码至Podfile文件:
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
最后,运行下面的代码下载Alamofire和SwiftyJSON:
pod install
如今回到Xcode!注意开发app时要保持打开CDBarcodes.xcworkspace(工做区)。
苹果的AV Foundation框架提供了咱们开发这个条形码阅读器app须要的相关工具。下面是整个过程当中会涉及到的几个方面:
AVCaptureSession将会处理来自相机的输入输出数据。
AVCaptureDevice指的是物理设备及其它的属性。AVCaptureSession从AVCaptureDevice这里接受输入信息。
AVCaptureDeviceInput从输入设备获取输入数据。
AVCaptureMetadataOutput将元数据对象发送至代理对象(delegate object)处进行处理。
在BarcodeReaderViewController.swift
里面,咱们的第一步操做是导入AVFoundation。
import UIKit import AVFoundation
注意要遵循AVCaptureMetadataOutputObjectsDelegate
。
在viewDidLoad()
,将运行咱们的条形码阅读引擎。
首先,新建一个AVCaptureSession
对象并设置AVCaptureDevice
。而后,咱们新建一个输入对象并添加至AVCaptureSession
。
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() }
若是设备碰巧没有摄像头时,扫描过程将不可能实现。所以,咱们须要一个报错函数。在这里,咱们通知用户寻找一个有相机的iOS设备以便进行下一步CD条形码的读取。
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 }
回到viewDidLoad()
,在将输入添加至(session)模块后,咱们接着新建AVCaptureMetadataOutput
并将它添加到模块中。咱们将捕捉到的数据经过一个串行序列的形式发送给代理对象。
下一步就是明确咱们应该扫描的条形码类型。在这里咱们面对的是EAN-13类型的条形码。有趣的是,并非全部的条形码都是这种类型;有一些将会是UPC-A格式。这可能会致使错误出现。
苹果会自动将UPC-A格式的条形码前面加一个0后转为EAN-13格式。UPC-A格式的条形码仅仅有12位数字;而在EAN-13格式的条形码中则是13位。这个自动转换过程的一个好处是咱们能够查询metadataObjectTypes AVMetadataObjectTypeEAN13Code
,所以两种格式的条形码咱们就都能读取了。须要注意的是这个转换会直接改变条形码从而误导Discogs数据库。不过不用担忧,咱们立刻就会解决这个问题。
不管如何,在用户设备相机有问题时咱们就将用户引导至scanningNotPossible()
函数。
// 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() }
如今咱们就搞定了这个酷炫的功能,拉出来溜溜吧!咱们将使用AVCaptureVideoPreviewLayer
以整个屏幕展现视频。
最后,咱们开始捕捉模块。
// 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()
In captureOutput:didOutputMetadataObjects:fromConnection
, we celebrate, as our barcode reader found something!
经过captureOutput:didOutputMetadataObjects:fromConnection
,咱们的条形码阅读器终于读取到了一些数据。
首先,咱们须要使用第一个对象得到metadataObjects
数组并将其转换为可机读代码。而后,咱们将readableCode
字符串发送至barcodeDetected()
。
在进入barcodeDetected()
函数前,咱们会中止捕捉模块并给用户一个震动反馈。若是咱们忘了叫停捕捉模块,那么震动也就停不下来了!这也是为何这是一个好案例的缘由。
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() } }
在barcodeDetected()
函数里面咱们有不少事情要作。第一个任务是在震动反馈以后,提示用户咱们已经发现了条形码。而后咱们利用找到的数据开始干活!
条形代码中的空格必须移除。在这以后咱们须要确认条形码格式是EAN-13仍是UPC-A。若是是EAN-13咱们能够直接使用。若是对象是一个UPC-A代码,那么它已经被转化为EAN-13格式,咱们须要将其转换为原始格式。
如咱们前文已经讨论的那样,苹果设备在UPC-A格式的条形码前添加一个0将其转化为EAN-13格式,所以咱们首先肯定代码是以0开头的。若是是,咱们须要将它移除。少了这一步,Discogs数据库将不能识别这个数字,咱们也就得不到想要的数据了。
在得到清理后的条形码字符串后,咱们将它发送至DataService.searchAPI()
并弹出BarcodeReaderViewController.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) }
在离开BarcodeReaderViewController.swift
以前,在viewDidLoad()
下面,咱们添加 viewWillAppear()
和 viewWillDisappear()
函数。viewWillAppear()
将会开启捕捉模块;而viewWillDisappear()
会终止这一模块。
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() } }
在DataService.swift
里,咱们首先要导入Alamofire 和 SwiftyJSON。
接着,咱们声明一些变量以便存储从Discogs返回的原始数据。根据Bionik6的建议,咱们巧妙地使用private(set)
函数避免了用户可能致使的阻塞问题。
而后,创建Alamofire GET请求。在这里JSON会被解析,从而得到专辑的title(名称)和year(发行年份)。将原始的title和year字符串赋给ALBUM_FROM_DISCOGS
和 YEAR_FROM_DISCOGS
,在后文将会用到它们来初始化咱们的专辑。
如今,咱们拥有了来自Discogs的数据,咱们能够正式开秀了;随之咱们通知AlbumDetailsViewController.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) } } }
在专辑模块Album.swift
中,咱们会处理专辑数据以便符合咱们的要求。这个模块将会获取原始的artistAlbum
和 albumYear
字符串而后将它们用户友好化。在AlbumDetailsViewController.swift
咱们展现加工后的album
和 year
信息。
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)" } }
在viewDidLoad()
模块中,设置好指向条形码阅读器的标签(label)。而后,咱们须要在NSNotification
添加观察者(Observer),以便咱们已经展现的提示可以集群。在deinit
中,咱们会移除观察者(Observer)。
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) }
当通知出现时,setLabels()
函数将会被调用。在这里,咱们会使用来自DataService.swift
的原始数据初始化Album
。标签将会展现加工后的字符串。
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)" }
应用搭建完毕,扫一下CD的条形码咱们就能肯定专辑的名称,艺人和发行年份信息,这颇有意思!为了更好的测试CDBarcodes,咱们能够随机找一些CD或是黑胶唱片。这样咱们就更有机会同时遇到EAN-13和UPC-A两种条形码格式的案例。目前咱们二者都能处理!
为了使应用顺利运行至BarcodeReaderViewController模块,注意避免闪光以确保相机能捕捉到条形码信息。
这里是完整代码的下载连接。
不论是商人,机智的消费者仍是通常人士,这个条形码阅读器都很实用。所以,开发者拿这个案例来练练手是极好的。
可是咱们也看到有趣的仅仅是扫码部分。在得到数据后,咱们遇到了一点小问题,如EAN-13 和 UPC-A格式问题。咱们找到了解决问题应对需求的办法。
接下来,咱们能够探讨一些其余的metadataObjectTypes
以及一些新API。机会无穷,经验无价。
本文系 OneAPM 工程师编译整理。OneAPM Mobile Insight 以真实用户体验为度量标准进行 Crash 分析,监控网络请求及网络错误,提高用户留存。访问 OneAPM 官方网站感觉更多应用性能优化体验,想阅读更多技术文章,请访问 OneAPM 官方技术博客。
本文转自 OneAPM 官方博客
原文连接:http://www.appcoda.com/simple-barcode-reader-app-swift/