使用 Swift 建立簡單的二維碼掃描應用

做者:AppCoda,原文連接,原文日期:2016-05-19
譯者:Prayer;校對:numbbbbb;定稿:CMBios

排着長隊等待結帳的商店,幫助旅客記錄包裹和航班信息的機場,幫助大型零售商處理大量無聊的存貨清單,這些場景很是適合使用條碼掃描器。此外,條碼掃描器也能幫消費者進行智能購物和產品分類。既然它這麼棒,不如咱們在 iPhone 上作一個吧!git

幸運的是,對 Apple 開發者來講,實現條碼掃描很是容易,蘋果大法好!咱們會使用 AV Foundation 來實現一個小巧的 iPhone app,可以掃描 CD 上的條碼,獲取專輯的一些重要信息,並將內容輸出到 App 視圖中。可以實現讀取條碼的功能,這很是的酷,可是咱們的野心不止於此,咱們會對識別的條碼內容做進一步的操做。github

我本不應再多囉嗦,不過仍是友情提醒一下,這個條碼掃描 app 只有在設備具備攝像頭時才能正確工做。記住這一點,準備一臺有攝像頭的 iOS 設備,咱們開始吧!數據庫

關於 CDBarcodes

今天咱們建立的應用叫作 CDBarcodes —— 它仍是很智能的。當設備掃描到一個條碼時,咱們會將處理後的條碼內容發送給 Discogs 數據庫,而後得到專輯的名稱、藝術家以及發佈年份。Discogs 的數據庫中有大量的音樂數據,因此咱們基本上能查到全部數據。json

從這裏下載 CDBarcodes 的 starter projectswift

Discogs

先從 Discogs 開始。首先,咱們須要登陸或者註冊一個 Discogs 帳戶。登陸以後,拉到網站的最底端,在 footer 的最左邊邊欄,點擊 API。api

在 Discogs API 頁面,點擊左邊欄 Database 中的 Search。數組

這個就是咱們將會用到的 API。咱們使用 「title」 和 「year」 參數來獲取專輯信息。xcode

如今咱們須要將查詢的 URL 保存到咱們的 CDBarcodes 中。在 Constants.swift 文件中,將 https://api.discogs.com/database/search?q= 添加到常量 DISCOGS_AUTH_URL 中。ruby

let DISCOGS_AUTH_URL = "https://api.discogs.com/database/search?q="

如今咱們能夠很方便地在應用中使用 DISCOGS_AUTH_URL 獲取查詢 URL。

回到剛纔的 Discogs API 網站。咱們須要建立一個新應用,取得 API 的使用資格。在導航欄中,網頁的最頂部,點擊 Create an App。以後點擊 Create an Application 按鈕。

應用名稱的話,輸入 「CDBarcodes + 你的名字」,或者其餘你喜歡的名字。description 字段能夠寫:

「This is an iOS app that reads barcodes from CDs and displays information about the albums.」

譯註:「這個 iOS 應用會讀取 CD 的條形碼並顯示唱片信息。」

最後,點擊 Create Application 按鈕。

在最後的結果頁面,咱們可以獲得使用條碼來作一些操做的資格信息。

拷貝 Consumer Key,粘貼到 Constants.swift 文件的 DISCOGS_KEY 中。再拷貝 Consumer Secret,粘貼到 Constants.swift 文件的 DISCOGS_SECRET 中。

同 URL 同樣,如今咱們能夠在應用中很方便地使用這些變量了。

CocoaPods

爲了可以和 Discogs API 通訊,咱們使用一個優秀的第三方庫管理工具:CocoaPods。若是想要了解更多關於 CocoaPods 的信息,或者想學習如何安裝它,能夠到它的官網查詢。

有了 CocoaPods 就能夠安裝第三方庫,咱們會使用 Alamofire 來請求網絡,使用 SwiftyJSON 來處理從 Discogs 返回的 JSON 數據。

下面咱們把這兩個庫引入到 CDBarcodes 工程中!

CocoaPods 安裝好以後,打開終端,進入 CDBarcodes 目錄,初始化 CocoaPods,命令以下:

bash
cd <your-xcode-project-directory>
pod init

使用 Xcode 打開 Podfile:

bash
open -a Xcode Podfile

將下面內容拷貝到 Podfile 中:

ruby
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:

bash
pod install

如今讓咱們回到 Xcode 中!切記要打開的是 CDBarcodes.xcworkspace

識別條碼

AV Foundation 框架提供了識別條碼的工具。咱們來大概描述一下工做原理。

  • AVCaptureSession 會管理從攝像頭獲取的數據——將輸入的數據轉爲可使用的輸出

  • AVCaptureDevice 表示物理設備和其餘屬性。AVCaptureSession 會從 AVCaptureDevice 獲取輸入數據

  • AVCaptureDeviceInput 從設備中捕獲數據

  • AVCaptureMetadataOutput 會向處理數據的 delegate 轉發得到的元數據

BarcodeReaderViewController.swift 文件中,首先導入 AVFoundation

import UIKit
import AVFoundation

同時,咱們須要遵循 AVCaptureMetadataOutputObjectsDelegate 協議。

viewDidLoad() 中,咱們要發動條碼掃描引擎。

首先,建立一個 AVCaptureSession 對象,而後設置 AVCaptureDevice。以後咱們將建立一個輸入對象(input object),而後將其加入到 AVCaptureSession 中。

class BarcodeReaderViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {

var session: AVCaptureSession!
var previewLayer: AVCaptureVideoPreviewLayer!

override func viewDidLoad() {
    super.viewDidLoad()
    
    // 建立一個 session 對象
    session = AVCaptureSession()
    
    // 設置 captureDevice.
    let videoCaptureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
    
    // 建立 input object.
    let videoInput: AVCaptureDeviceInput?
    
    do {
        videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
    } catch {
        return
    }
    
    // 將 input 加入到 session 中
    if (session.canAddInput(videoInput)) {
        session.addInput(videoInput)
    } else {
        scanningNotPossible()
    }

若是你的設備沒有攝像頭,那就沒法掃描條碼。咱們添加了一個處理失敗場景的方法。若是沒有攝像頭,會彈出一個提示框來提示用戶,換一個有攝像頭的設備來掃描 CD 的條碼。

func scanningNotPossible() {
    // 告知用戶該設備沒法進行條碼掃描
    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() 方法中,將 input 添加到 session 以後,咱們須要建立 AVCaptureMetadataOutput 並把它也添加到 session 中。咱們會將捕獲到的數據經過串行隊列發送給 delegate 對象。

下一步須要聲明咱們將要掃描的條碼類型。對咱們而言,咱們須要使用 EAN-13 條碼。有意思的是,咱們掃描的條碼並不是都是 EAN-13 類型的;一些有多是 UPC-A 類型,這可能會形成識別的問題。

Apple 經過在前面加上 0 來將 UPC-A 條碼轉換爲 EAN-13 條碼。UPC-A 條碼只有 12 位,EAN-13 條碼,和你猜想的同樣,是 13 位。這個自動轉化特性的好處是,咱們在設置 metadataObjectTypes 時,只要設置爲 AVMetadataObjectTypeEAN13Code,EAN-13 和 UPC-A 條碼都將會被識別。不過這會修改條碼,所以有可能會在查詢 Discogs 時出問題,後面咱們會處理這個問題。

若是攝像頭有問題,咱們須要使用 scanningNotPossible() 來告知用戶。

// 建立 output 對象
let metadataOutput = AVCaptureMetadataOutput()

// 將 output 對象添加到 session 上
if (session.canAddOutput(metadataOutput)) {
   session.addOutput(metadataOutput)
   
   // 經過串行隊列,將捕獲到的數據發送給相應的代理
   metadataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue())
   
   // 設置可掃描的條碼類型
   metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeEAN13Code]
   
} else {
   scanningNotPossible()
}

咱們已經擁有了掃描條碼的強大能力,如今須要作的是預覽掃描畫面。使用 AVCaptureVideoPreviewLayer 在整個屏幕上顯示拍攝到的畫面。

而後,咱們就能夠開始掃描了。

// 添加 previewLayer 讓其顯示攝像頭拍到的畫面
​        
previewLayer = AVCaptureVideoPreviewLayer(session: session);
previewLayer.frame = view.layer.bounds;
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
view.layer.addSublayer(previewLayer);
    
// 開始運行 session
    
session.startRunning()

captureOutput:didOutputMetadataObjects:fromConnection 方法中,咱們能夠慶祝一下,由於執行到該方法就說明已經識別了一些信息。

首先,咱們須要從 metadataObjects 數組中取出第一個對象,而後將其轉化爲機器能夠識別的格式。而後將轉換後的 readableCode 做爲一個 string 值傳入 barcodeDetected() 方法中。

在看 barcodeDetected() 方法以前,咱們須要以震動的形式給用戶一些掃描成功的反饋而且關閉 session(stop the session)。萬一你忘記關閉了 session,不要緊,你的設備會一直震動,直到你關閉爲止。

func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {
 
    // 從 metadataObjects 數組中取得第一個對象
    if let barcodeData = metadataObjects.first {
        // 將其轉化爲機器能夠識別的格式
        let barcodeReadable = barcodeData as? AVMetadataMachineReadableCodeObject;
        if let readableCode = barcodeReadable {
            // 將 readableCode 做爲一個 string 值,傳入 barcodeDetected() 方法中
            barcodeDetected(readableCode.stringValue);
        }
        
        // 以震動的形式告知用戶,識別成功        AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
        
        // 關閉 session (避免你的設備一直嗡嗡震動)
        session.stopRunning()
    }
}

咱們須要在 barcodeDetected() 中作一些操做。第一個任務是彈出一個提示框告知用戶,咱們掃描到了一個條碼。而後將掃描到的信息轉化爲咱們須要的內容。

必須去掉掃描內容中的空格。去掉空格以後,咱們須要判斷條碼是 EAN-13 仍是 UPC-A 類型。若是是 EAN-13 類型,不須要額外的操做。若是是 UPC-A 條碼,它被轉化爲了 EAN-13 類型,咱們須要把它還原成原有的格式。

就像咱們以前討論的那樣,蘋果在 UPC-A 條碼的前頭加上一個 0 來將其轉換爲 EAN-13,因此咱們須要判斷其是否以 0 開頭,若是是的話,刪掉它。若是沒有這一步,Discogs 沒法識別這個數字,咱們也沒有辦法獲得正確的數據。

拿處處理後的條碼數據以後,咱們將它傳給 DataService.searchAPI() 而後顯示 BarcodeReaderViewController

func barcodeDetected(code: String) {
 
    // 讓用戶知道,咱們掃描到了
    let alert = UIAlertController(title: "Found a Barcode!", message: code, preferredStyle: UIAlertControllerStyle.Alert)
    alert.addAction(UIAlertAction(title: "Search", style: UIAlertActionStyle.Destructive, handler: { action in
        
        // 去除空格
        let trimmedCode = code.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
        
        // 判斷是 EAN 仍是 UPC?
        
        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()
            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() 方法中,咱們讓 session 開始運行。相應的,在 viewWillDisappear() 方法中,讓 session 中止運行。

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 獲得專輯的名稱和年份。咱們分別把獲得的名稱和年份原始數據賦值給 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) {
        
        // 從 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
                
                // 發送通知,讓 AlbumDetailsViewController 知道咱們獲得了數據
                NSNotificationCenter.defaultCenter().postNotificationName("AlbumNotification", object: nil)
        }
    }

}

Album 模型

在專輯的數據模型 Album.swift 中,須要將專輯模型轉化爲咱們想要的數據。這個模型接受原始的 artistAlbumalbumYear 數據,把它們轉換爲更加易讀的數據。

import Foundation
 
class Album {        
 
private(set) var album: String!
private(set) var year: String!
 
init(artistAlbum: String, albumYear: String) {
    
    // 爲專輯信息添加一些額外的數據
    self.album = "Album: \n\(artistAlbum)"
    self.year = "Released in: \(albumYear)"
}
 
}

是時候秀一波專輯數據了!

viewDidLoad() 方法中,設置 labels 的內容,提示用戶開始掃描。咱們須要添加 observer 來監聽 NSNotification 從而接收通知。同時須要在 deinit 中移除監聽者。

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 對象。而後將 label 中的內容設置爲咱們想要的 Album 內容。

func setLabels(notification: NSNotification){

    // 使用 DataService.swift 中的數據初始化 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

咱們的 app 完成啦!固然,咱們能夠直接從 CD 封面看到專輯名稱、藝術家和發行年份,可是用咱們的 app 要有趣得多!爲了更好地測試 CDBarcodes 應用,咱們須要找一些 CD 和唱片。這樣就有可能同時遇到 EAN-13 和 UPC-A 條碼,真正發揮 app 的威力。

BarcodeReaderViewController 中,注意將相機對焦到條碼上。

這裏是完成以後的 CDBarcodes 代碼。

總結

不管是商務人士、購物者仍是普通人,條碼掃描器都一個特別有用的工具。所以,可以開發條碼掃描也很是有用。

掃描那部分比較有趣。在得到掃描的數據以後,咱們須要對數據作進一步操做,例如判斷是 EAN-13 仍是 UPC-A 類型。咱們須要找到轉化數據的正確方式,而後老司機就上路了。

若是想了解更多內容,能夠讀取其餘的 metadataObjectTypes 和一些新 API。惟一的限制就是你的想象力。

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索