如何使用 Swift 开发简单的条形码检测器?
【编者按】本文作者为 Matthew Maher,主要手把手地介绍如何用 Swift 构建简单的条形码检测器。文章系 OneAPM 工程师编译整理。
超市收银员对货物进行扫码,机场内录入行李或检查乘客,或是在大型零售商的存货管理等活动中,条形码扫码器都是一个简单而实用的工具。事实上,条形码扫码器还帮助消费者实现了智能购物,货物分类等用途。这次,我们将为iPhone开发一个扫码器。
我们很幸运,苹果公司让条形码扫描过程的实现变得很简单。我们将会深入AV Foundation框架开发一个简单的能够扫描CD条形码的app,然后获得专辑的关键信息,最后在app的界面中打印出来。阅读条形码很酷炫也很重要,我们会根据读到的条形码采取进一步的操作。
不用多说,能扫码的设备必须要有一个摄像头。从这里开始,让我们拿一个配备有摄像头的iOS设备开始干活吧!
简介 CDbarcodes
我们今天开发的这个app名叫CDBarcodes——通俗易懂,即条形码扫描对象是CD。当我们的设备检测到一个条形码时,会拾取这个货码然后发送到Discogs的数据库,获得其专辑名称、艺人姓名以及发布年份。Discogs的音乐数据库十分强大,因此我们很有可能找到一些实用信息。
下载CDBarcodes的初始项目。
除了一个不错的数据库,Discogs还有一个实用的API来帮助查询。我们涉及的仅仅是Discogs提供给开发者的一小部分功能,不过这已经足够使我们的app跑起来了。
Discogs
进入Discogs网站。首先我们必须注册一个Discogs账号并登录。在这之后,下拉到页面最底端。在页尾最左栏点击API。
在Discogs的API界面左侧的数据库区域点击搜索(Search)。
这是我们查询的端点。我们将会从“title”和“year”这两个参数上获得专辑信息。
现在,我们将这个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应用里使用这些参数。
CocoaPods
我们使用功能强大的依赖管理器(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. 设置captureDevicelet 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-13metadataOutput.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: Stringif trimmedCodeString.hasPrefix("0") && trimmedCodeString.characters.count > 1 {trimmedCodeNoZero = String(trimmedCodeString.characters.dropFirst())// Send the doctored UPC to DataService.searchAPI() 将UPC发送至APIDataService.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 SwiftyJSONclass 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 invar json = JSON(response.result.value!)let albumArtistTitle = "\(json["results"][0]["title"])"let albumYear = "\(json["results"][0]["year"])"self.dataService.ALBUM_FROM_DISCOGS = albumArtistTitleself.dataService.YEAR_FROM_DISCOGS = albumYear// Post a notification to let AlbumDetailsViewController know we have some data. 通知AlbumDetailsViewControllerNSNotificationCenter.defaultCenter().postNotificationName("AlbumNotification", object: nil)}
}}
专辑模块
在专辑模块Album.swift
中,我们会处理专辑数据以便符合我们的要求。这个模块将会获取原始的artistAlbum
和 albumYear
字符串然后将它们用户友好化。在AlbumDetailsViewController.swift
我们展示加工后的album
和 year
信息。
import Foundationclass 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)"
}
测试 CDBarcodes
应用搭建完毕,扫一下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/
如何使用 Swift 开发简单的条形码检测器?相关推荐
- 《Swift开发实战》——第1章,第1.4节启动iOS 8模拟器
本节书摘来自异步社区<Swift开发实战>一书中的第1章,第1.4节启动iOS 8模拟器,作者 李宁,更多章节内容可以访问云栖社区"异步社区"公众号查看 1.4 启动i ...
- swift int转string_用Swift开发macOS程序,九、目录模块
程序中跟目录模块相似的,也是通过使用Outline View组件显示内容的模块有:备注.搜索.角色.符号.字典等其它五个.设计完成本模块后将不再对其它模块进行说明,一切请查看Github上代码.目录模 ...
- iOS开发------简单实现图片多选功能(Photos.framework篇)
Photos.framework是iOS8后苹果推出的一套替代AssetsLibrary.framework获取相册资源的原生库,至于AL库,欢迎大家给博文iOS开发--简单实现图片多选功能(Asse ...
- linux swift开发环境,Linux平台swift语言开发学习环境搭建
摘要 介绍在Ubuntu平台搭建Swift开发环境: 介绍Swift脚本解析器swift的使用: 介绍Swift编译器swiftc的使用: 1.序言 这两天一直忙,没来得及记录东西,周三12月4日凌晨 ...
- 1、swift开发iOS——基础
swift开发iphone app SWIFT Swift 是一种支持多编程范式和编译式的开源编程语言,苹果于2014年WWDC(苹果开发者大会)发布,用于开发 iOS,OS X 和 watchOS ...
- 关于《Swift开发指南》背后的那些事
时间轴(倒叙) 2014年8月底 在图灵出版社的大力支持下,全球第一本全面.系统.科学的,包含本人多年经验的呕心沥血之作<Swift开发指南>(配有同步视频课程和同步练习)全线重磅推出 2 ...
- 使用Swift开发一个MacOS的菜单状态栏App
新媒体管家 点击上方"程序员大咖",选择"置顶公众号" 关键时刻,第一时间送达! 下面开始介绍如何使用Swift开发一个Mac Menu Bar(Status ...
- [绍棠] Scrapy+Flask+Mongodb+Swift开发全攻略
Scrapy+Flask+Mongodb+Swift开发全攻略 先一一介绍一下上面4个东西.第一个叫做Scrapy的东西是用python写的爬虫框架. Flask是python写的一个非常有名的web ...
- 写在新书《Swift开发手册:技巧与实战》出版之际
1月份的时候新书出版了,正值研究生毕业之际,想写点什么又无法抵抗毕业浮躁的心态,所以推到了正式入职之后.首先当然还是想安利一波,新书封面见如下: 2014年年底因为跟同学组队参加竞赛的关系,踏入了iO ...
最新文章
- Mybatis之一级缓存,二级缓存
- 华为mate40怎么用鸿蒙,怎么使用鸿蒙系统?
- [YTU]_2444( C++习题 对象转换)
- 来场产品设计师的对决吧!MacBook、大疆OSMO等你拿
- 一名运营,自学一年前端,成功入职杭州某独角兽企业,他的面试经验和学习方法等分享...
- 前端学习(669):流程控制
- 博客园山寨版(asp.net mvc 开源)
- 模版 ----- 一维指数型枚举 排列型枚举 组合型枚举
- AS/400开发经验点滴(六)如何制作下拉菜单
- R语言和Python交互
- win10系统怎么查看密钥?
- 中望3D 2021 自动缩放基准面大小
- 机电一体化仿真--手爪
- Jquery选择器:通过class名获取ID
- DistilBERT, a distilled version of BERT: smaller, faster, cheaper and lighter学习
- IBM 中国研究院面试经历
- 趣图:当计算机可以更新的时候
- 跨平台下移动应用的开发框架对比与分析
- MySQL数据库快速入门到精通(超详细保姆级,建议收藏)这可能是目前最适合你的教程,从基础语法到实例演示。
- A2B音频总线在智能座舱中的应用
热门文章
- linux 网络慢 dns,Linux DNS客户端解析域名慢解决
- PhoneGap 微信插件 for iOS
- LeCo-83.删除排序链表中的重复元素
- 构建全渠道零售平台及营销场景解读
- 建筑平面布置与防火防烟分区(二)
- 三菱模拟量fx3u4da_三菱模拟量输入模块FX3U-4AD与FX3U-4AD-ADP的区别
- java 调停者模式_[Java教程]《JAVA与模式》之调停者模式
- 售价对标奢侈品,国货香水“德不配位”?
- vue cli js css压缩方案
- edger多组差异性分析_用R实现批量差异分析(t检验和方差分析),自己算P值