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

Prayer - swiftgg發表於2016-07-26

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

幸運的是,對 Apple 開發者來說,實現條碼掃描非常容易,蘋果大法好!我們會使用 AV Foundation 來實現一個小巧的 iPhone app,能夠掃描 CD 上的條碼,獲取專輯的一些重要資訊,並將內容輸出到 App 檢視中。能夠實現讀取條碼的功能,這非常的酷,但是我們的野心不止於此,我們會對識別的條碼內容作進一步的操作。

我本不該再多囉嗦,不過還是友情提醒一下,這個條碼掃描 app 只有在裝置具有攝像頭時才能正確工作。記住這一點,準備一臺有攝像頭的 iOS 裝置,我們開始吧!

關於 CDBarcodes

今天我們建立的應用叫做 CDBarcodes —— 它還是很智慧的。當裝置掃描到一個條碼時,我們會將處理後的條碼內容傳送給 Discogs 資料庫,然後獲得專輯的名稱、藝術家以及釋出年份。Discogs 的資料庫中有大量的音樂資料,所以我們基本上能查到所有資料。

從這裡下載 CDBarcodes 的 starter project

Discogs

先從 Discogs 開始。首先,我們需要登入或者註冊一個 Discogs 賬戶。登入之後,拉到網站的最底端,在 footer 的最左邊邊欄,點選 API。

在 Discogs API 頁面,點選左邊欄 Database 中的 Search。

這個就是我們將會用到的 API。我們使用 “title” 和 “year” 引數來獲取專輯資訊。

現在我們需要將查詢的 URL 儲存到我們的 CDBarcodes 中。在 Constants.swift 檔案中,將https://api.discogs.com/database/search?q= 新增到常量 DISCOGS_AUTH_URL 中。

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,命令如下:

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 中!切記要開啟的是 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_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) {

	    // 從 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 中,需要將專輯模型轉化為我們想要的資料。這個模型接受原始的 artistAlbum 和albumYear 資料,把它們轉換為更加易讀的資料。

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。唯一的限制就是你的想象力。

相關文章