【編者按】本文做者爲 Matthew Maher,主要手把手地介紹如何用 Swift 構建簡單的條形碼檢測器。文章系 OneAPM 工程師編譯整理。html
超市收銀員對貨物進行掃碼,機場內錄入行李或檢查乘客,或是在大型零售商的存貨管理等活動中,條形碼掃碼器都是一個簡單而實用的工具。事實上,條形碼掃碼器還幫助消費者實現了智能購物,貨物分類等用途。此次,咱們將爲iPhone開發一個掃碼器。ios
咱們很幸運,蘋果公司讓條形碼掃描過程的實現變得很簡單。咱們將會深刻AV Foundation框架開發一個簡單的可以掃描CD條形碼的app,而後得到專輯的關鍵信息,最後在app的界面中打印出來。閱讀條形碼很酷炫也很重要,咱們會根據讀到的條形碼採起進一步的操做。git
不用多說,能掃碼的設備必需要有一個攝像頭。從這裏開始,讓咱們拿一個配備有攝像頭的iOS設備開始幹活吧!github
咱們今天開發的這個app名叫CDBarcodes——通俗易懂,即條形碼掃描對象是CD。當咱們的設備檢測到一個條形碼時,會拾取這個貨碼而後發送到Discogs的數據庫,得到其專輯名稱、藝人姓名以及發佈年份。Discogs的音樂數據庫十分強大,所以咱們頗有可能找到一些實用信息。數據庫
下載CDBarcodes的初始項目。json
除了一個不錯的數據庫,Discogs還有一個實用的API來幫助查詢。咱們涉及的僅僅是Discogs提供給開發者的一小部分功能,不過這已經足夠使咱們的app跑起來了。swift
進入Discogs網站。首先咱們必須註冊一個Discogs帳號並登陸。在這以後,下拉到頁面最底端。在頁尾最左欄點擊API。api
在Discogs的API界面左側的數據庫區域點擊搜索(Search)。數組
這是咱們查詢的端點。咱們將會從「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」,點擊該按鈕。
在應用名稱欄裏輸入「CDBarcodes Your Name」,或是其餘合適的名字。描述可使用下面的文字:
「這是一個iOS應用,旨在在讀取CD的條形碼後顯示專輯信息。」
而後,點擊「Create Application」(即建立應用)按鈕。
在結束頁面,會看到容許咱們使用條形碼的認證信息。
複製「Consumer Key」(用戶祕鑰)到Constants.swift
的DISCOGS_KEY
裏面。
有了這個URL,咱們能夠很方便的在整個CDBarcodes應用裏使用這些參數。
咱們使用功能強大的依賴管理器(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)" }
應用搭建完畢,掃一下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/