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

Chl54T 8年前發布 | 9K 次閱讀 Swift Apple 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. 設置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)"
}

測試 CDBarcodes

應用搭建完畢,掃一下CD的條形碼我們就能確定專輯的名稱,藝人和發行年份信息,這很有意思!為了更好的測試CDBarcodes,我們可以隨機找一些CD或是黑膠唱片。這樣我們就更有機會同時遇到EAN-13和UPC-A兩種條形碼格式的案例。目前我們兩者都能處理!

為了使應用順利運行至BarcodeReaderViewController模塊,注意避免閃光以確保相機能捕捉到條形碼信息。

這里是完整代碼的 下載鏈接

結論

不管是商人,機智的消費者還是一般人士,這個條形碼閱讀器都很實用。因此,開發者拿這個案例來練練手是極好的。

但是我們也看到有趣的僅僅是掃碼部分。在獲得數據后,我們遇到了一點小問題,如EAN-13 和 UPC-A格式問題。我們找到了解決問題應對需求的辦法。

接下來,我們可以探討一些其他的 metadataObjectTypes 以及一些新API。機會無窮,經驗無價。

本文系OneAPM 工程師編譯整理。 OneAPM Mobile Insight 以真實用戶體驗為度量標準進行Crash 分析,監控網絡請求及網絡錯誤,提升用戶留存。訪問OneAPM 官方網站感受更多應用性能優化體驗,想閱讀更多技術文章,請訪問OneAPM 官方技術博客。

來自: http://blog.oneapm.com/apm-tech/758.html

 

 本文由用戶 Chl54T 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!