如何使用 Swift 開發簡單的條形碼檢測器?

【編者按】本文做者爲 Matthew Maher,主要手把手地介紹如何用 Swift 構建簡單的條形碼檢測器。文章系 OneAPM 工程師編譯整理。html

超市收銀員對貨物進行掃碼,機場內錄入行李或檢查乘客,或是在大型零售商的存貨管理等活動中,條形碼掃碼器都是一個簡單而實用的工具。事實上,條形碼掃碼器還幫助消費者實現了智能購物,貨物分類等用途。此次,咱們將爲iPhone開發一個掃碼器。ios

咱們很幸運,蘋果公司讓條形碼掃描過程的實現變得很簡單。咱們將會深刻AV Foundation框架開發一個簡單的可以掃描CD條形碼的app,而後得到專輯的關鍵信息,最後在app的界面中打印出來。閱讀條形碼很酷炫也很重要,咱們會根據讀到的條形碼採起進一步的操做。git

不用多說,能掃碼的設備必需要有一個攝像頭。從這裏開始,讓咱們拿一個配備有攝像頭的iOS設備開始幹活吧!github

簡介 CDbarcodes

咱們今天開發的這個app名叫CDBarcodes——通俗易懂,即條形碼掃描對象是CD。當咱們的設備檢測到一個條形碼時,會拾取這個貨碼而後發送到Discogs的數據庫,得到其專輯名稱、藝人姓名以及發佈年份。Discogs的音樂數據庫十分強大,所以咱們頗有可能找到一些實用信息。數據庫

下載CDBarcodes的初始項目json

如何使用 Swift 開發簡單的條形碼檢測器?

除了一個不錯的數據庫,Discogs還有一個實用的API來幫助查詢。咱們涉及的僅僅是Discogs提供給開發者的一小部分功能,不過這已經足夠使咱們的app跑起來了。swift

Discogs

進入Discogs網站。首先咱們必須註冊一個Discogs帳號並登陸。在這以後,下拉到頁面最底端。在頁尾最左欄點擊API。api

如何使用 Swift 開發簡單的條形碼檢測器?

在Discogs的API界面左側的數據庫區域點擊搜索(Search)。數組

如何使用 Swift 開發簡單的條形碼檢測器?

這是咱們查詢的端點。咱們將會從「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」,點擊該按鈕。

如何使用 Swift 開發簡單的條形碼檢測器?

在應用名稱欄裏輸入「CDBarcodes Your Name」,或是其餘合適的名字。描述可使用下面的文字:

「這是一個iOS應用,旨在在讀取CD的條形碼後顯示專輯信息。」

而後,點擊「Create Application」(即建立應用)按鈕。

在結束頁面,會看到容許咱們使用條形碼的認證信息。

複製「Consumer Key」(用戶祕鑰)到Constants.swiftDISCOGS_KEY裏面。

有了這個URL,咱們能夠很方便的在整個CDBarcodes應用裏使用這些參數。

如何使用 Swift 開發簡單的條形碼檢測器?

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, 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_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/

相關文章
相關標籤/搜索