【编者按】本文作者为 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.swiftdisCOGS_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. 设置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,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_disCOGSYEAR_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中,我们会处理专辑数据以便符合我们的要求。这个模块将会获取原始的artistAlbumalbumYear字符串然后将它们用户友好化。在AlbumDetailsViewController.swift我们展示加工后的albumyear信息。

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)"
}

测试 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 开发简单的条形码检测器?的更多相关文章

  1. HTML5 Web缓存和运用程序缓存(cookie,session)

    这篇文章主要介绍了HTML5 Web缓存和运用程序缓存(cookie,session),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. iOS Swift上弃用后Twitter.sharedInstance().session()?. userName的替代方案

    解决方法如果您仍在寻找解决方案,请参阅以下内容:

  3. 使用Fabric SDK iOS访问Twitter用户时间线

    我试图在这个问题上挣扎两天.我正在使用FabricSDK和Rest工具包,试图为Twitter使用不同的RestAPIWeb服务.我可以使用具有authTokenSecret,authToken和其他值的会话对象的TWTRLogInButton成功登录.当我尝试获取用户时间线时,我总是得到失败的响应,作为:{“errors”:[{“code”:215,“message”:“BadAuthentic

  4. ios – 如何从Apple Watch调用iPhone上定义的方法

    有没有办法从Watchkit扩展中调用iPhone上的类中定义的方法?根据我的理解,目前在Watchkit和iPhone之间进行本地通信的方法之一是使用NSUserDefaults,但还有其他方法吗?

  5. cocoapods – 命令/ bin / sh失败,退出代码23

    适用于所有豆荚,无需豆荚但仍然是同样的错误.有任何想法吗?

  6. iOS编译库/框架链接器标记问题是设备编译而不是模拟器

    背景:我正在尝试使用带有cocoapods的Phonegap.Phonegap已经存在从命令行编译的问题所以我只是将Xcode与生成的项目文件一起使用(并且在使用cocoapods时我无论如何都必须这样做).我用我的问题向Github推送了一个复制的空项目:https://github.com/Dan2552/phonegap-reproducable-issue您可以在repo的根目录中的bui

  7. ios – CocoaPods CorePlot安装显示缺少的框架和丢失的文件

    我是CocoaPods的新手,我正在尝试在我的Podfile中使用此行安装CorePlot:当我在安装后打开.xcworkspace文件时,CorePlot会显示缺少的框架.我将这些链接的二进制文件安装到我的Xcode项目中,但这并没有解决问题.在CorePlot目标中,似乎没有单独的方法来安装框架.这不是唯一的问题.当我尝试在CorePlot库中引用文件时,即使我可以看到CorePlot目标中的文件,我也会收到文件未找到错误:我尝试了以下import语句:都导致同样的错误.我不知道这些问题是否相关.我应

  8. 通用iOS应用程序在TestFlight中的iPad Air 2上不兼容

    您应该为项目和目标设置“有效架构”:armv7,armv7s和arm64.

  9. ios – 如何在不创建新目标的情况下阻止`pod install`将libPods.a添加到“Link with Libraries”构建阶段?

    解决方法要防止集成,您可以使用:

  10. ios – CocoaPods的Pods.xcconfig与现有的冲突

    解决方法你仍然可以吃蛋糕和(部分)吃它.您的目标的xcconfig文件可以包含一个cocoapods:但是,您仍然无法进食的部分是您想要指定已在Pods.xcconfig中设置的标志.我找不到添加到Cocoapods已在所包含文件中设置的路径的方法,例如你想要这个:然而,这一行简单地删除了Cocoapods头部搜索路径,只留下MyOtherIncludes版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。

随机推荐

  1. Swift UITextField,UITextView,UISegmentedControl,UISwitch

    下面我们通过一个demo来简单的实现下这些控件的功能.首先,我们拖将这几个控件拖到storyboard,并关联上相应的属性和动作.如图:关联上属性和动作后,看看实现的代码:

  2. swift UISlider,UIStepper

    我们用两个label来显示slider和stepper的值.再用张图片来显示改变stepper值的效果.首先,这三个控件需要全局变量声明如下然后,我们对所有的控件做个简单的布局:最后,当slider的值改变时,我们用一个label来显示值的变化,同样,用另一个label来显示stepper值的变化,并改变图片的大小:实现效果如下:

  3. preferredFontForTextStyle字体设置之更改

    即:

  4. Swift没有异常处理,遇到功能性错误怎么办?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. 字典实战和UIKit初探

    ios中数组和字典的应用Applicationschedule类别子项类别名称优先级数据包contactsentertainment接触UIKit学习用Swift调用CocoaTouchimportUIKitletcolors=[]varbackView=UIView(frame:CGRectMake(0.0,0.0,320.0,CGFloat(colors.count*50)))backView

  6. swift语言IOS8开发战记21 Core Data2

    上一话中我们简单地介绍了一些coredata的基本知识,这一话我们通过编程来实现coredata的使用。还记得我们在coredata中定义的那个Model么,上面这段代码会加载这个Model。定义完方法之后,我们对coredata的准备都已经完成了。最后强调一点,coredata并不是数据库,它只是一个框架,协助我们进行数据库操作,它并不关心我们把数据存到哪里。

  7. swift语言IOS8开发战记22 Core Data3

    上一话我们定义了与coredata有关的变量和方法,做足了准备工作,这一话我们来试试能不能成功。首先打开上一话中生成的Info类,在其中引用头文件的地方添加一个@objc,不然后面会报错,我也不知道为什么。

  8. swift实战小程序1天气预报

    在有一定swift基础的情况下,让我们来做一些小程序练练手,今天来试试做一个简单地天气预报。然后在btnpressed方法中依旧增加loadWeather方法.在loadWeather方法中加上信息的显示语句:运行一下看看效果,如图:虽然显示出来了,但是我们的text是可编辑状态的,在storyboard中勾选Editable,再次运行:大功告成,而且现在每次单击按钮,就会重新请求天气情况,大家也来试试吧。

  9. 【iOS学习01】swift ? and !  的学习

    如果不初始化就会报错。

  10. swift语言IOS8开发战记23 Core Data4

    接着我们需要把我们的Rest类变成一个被coredata管理的类,点开Rest类,作如下修改:关键字@NSManaged的作用是与实体中对应的属性通信,BinaryData对应的类型是NSData,CoreData没有布尔属性,只能用0和1来区分。进行如下操作,输入类名:建立好之后因为我们之前写的代码有些地方并不适用于coredata,所以编译器会报错,现在来一一解决。

返回
顶部