如何使用 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