iOS開發之二維碼掃描以及生成
簡介
- 二維條碼/二維碼是用某種 特定的幾何圖形 按一定規律在平面分布的黑白相間的圖形記錄數據符號信息的
- 在編碼上巧妙地利用構成計算機內部邏輯基礎的“0”、“1”比特流的概念,使用若干個與二進制相對應的幾何形體來表示文字數值信息
- 通過圖象輸入設備或光電掃描設備自動識讀以實現信息自動處理
特點
- 每種碼制有其特定的字符集
- 每個字符占有一定的寬度
- 具有一定的校驗功能
功能
- 信息獲取(名片、地圖、WIFI密碼、資料)
- 網站跳轉(跳轉到微博、手機網站、網站)
- 廣告推送(用戶掃碼,直接瀏覽商家推送的視頻、音頻廣告)
- 手機電商(用戶掃碼、手機直接購物下單)
- 防偽溯源(用戶掃碼、即可查看生產地;同時后臺可以獲取最終消費地)
- 優惠促銷(用戶掃碼,下載電子優惠券,抽獎)
- 會員管理(用戶手機上獲取電子會員信息、VIP服務)
- 手機支付(掃描商品二維碼,通過銀行或第三方支付提供的手機端通道完成支付)
目前越來越多的app使用了二維碼,且二維碼傳播更為快捷,方便。
iOS開發中也有諸如 ZBar 、 ZXing 這樣優秀的第三方框架,但是其不支持64位。今天就帶大家一起來自己動手寫一下二維碼掃描以及二維碼生成的這一功能。
就以新浪微博的二維碼掃描界面為例來進行講解。
一、掃描界面動畫
界面這一塊我是以storyboard來進行搭建的,想將更多的精力放在二維碼掃描以及生成上。
- 用 navigationController 包裝一個最普通的 viewController
- 然后再在 viewController 上依次加入如圖所示的3個子控件
界面基本搭建完成之后就要拖控件以及約束到對應控制器中來做掃描沖擊波的動畫了。
在 ZDQRCodeViewController.swift 中主要會對容器視圖的高度約束、沖擊波圖片頂部約束進行相應設置來進行動畫//沖擊波頂部約束 @IBOutlet weak var scanLineTopConstraint: NSLayoutConstraint! //容器視圖高度約束 @IBOutlet weak var containerViewHeightConstraint: NSLayoutConstraint!
- 首先設置沖擊波的頂部約束的 constant 等于負的容器視圖的高度約束的 constant ,然后再在一定時間內讓其等于正的容器視圖的高度約束的 constant 如此無限重復就會展現出想要的動畫效果。
//MARK:- 沖擊波動畫 extension ZDQRCodeViewController { func startAnimation() { //清空layer上所有動畫 scanfLineView.layer.removeAllAnimations() //初始化沖擊波位置 scanLineTopConstraint.constant = -containerViewHeightConstraint.constant view.layoutIfNeeded() //執行動畫 UIView.animate(withDuration: 2.0) { UIView.setAnimationRepeatCount(MAXFLOAT) self.scanLineTopConstraint.constant = self.containerViewHeightConstraint.constant self.view.layoutIfNeeded() } } }
還有一點需要特別注意的:上面定義的 startAnimation() 這個方法一定要在 func viewWillAppear(_ animated: Bool) 這個方法里面調用 否則看不到動畫效果
二、掃描二維碼
識別原理
輸入設備與輸出數據是通過拍攝會話 AVCaptureSession 來進行溝通的。
代碼實現
-
攝像頭輸入設備
//輸入設備 lazy var inputDevice : AVCaptureDeviceInput? = { let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) return try? AVCaptureDeviceInput(device: device) }()
-
拍攝會話
lazy var session = AVCaptureSession()
- 數據輸出
lazy var output = AVCaptureMetadataOutput()
- 設置預覽圖層
lazy var previewLayer : AVCaptureVideoPreviewLayer = { let layer = AVCaptureVideoPreviewLayer(session: self.session) return layer! }()
- 建立通道、設置會話
//MARK:- 二維碼相關 extension ZDQRCodeViewController : AVCaptureMetadataOutputObjectsDelegate { func setUpQRCode() { //判斷是否能將輸入設備添加至會話中 if !session.canAddInput(inputDevice) { return } //判斷是否能將輸出對象添加至會話中 if !session.canAddOutput(output) { return } //添加輸入輸出設備 session.addInput(inputDevice) session.addOutput(output) //設置輸出對象能夠解析的數據類型 output.metadataObjectTypes = output.availableMetadataObjectTypes //設置代理監聽解析后的數據 output.setMetadataObjectsDelegate(self, queue:DispatchQueue.main) //添加預覽圖層 previewLayer.frame = view.bounds view.layer.insertSublayer(previewLayer, at: 0) //開始掃描 session.startRunning() }
-
實現代理方法
func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) {
print(metadataObjects) }</code></pre> </li> </ul>
三、設置掃描區域
為了能夠擁有更好的用戶體驗,我們應該需要設置一下掃描的區域,讓用戶將二維碼放進界面中的掃描區域內再開始掃描
- 設置解析數據的區域(在output初始化時進行設置)
//輸出對象 lazy var output : AVCaptureMetadataOutput = { //創建輸出對象 let metadataOutput = AVCaptureMetadataOutput() //獲取容器視圖的frame let containerFrame = self.containerView.frame let screenFrame = UIScreen.main.bounds //計算比例 let X = containerFrame.origin.y / screenFrame.size.height let Y = containerFrame.origin.x / screenFrame.size.width let W = containerFrame.size.height / screenFrame.size.height let H = containerFrame.size.width / screenFrame.size.width //設置解析數據所感興趣的區域 metadataOutput.rectOfInterest = CGRect(x: X, y: Y, width: W, height: H) return metadataOutput }()
注意:蘋果默認為掃描二維碼時手機是處于橫屏的,所以計算比例時要將X,Y,W,H反過來 要像我上面代碼中的計算方式
四、設置描邊
-
坐標轉換
func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { for object in metadataObjects { let dataObject = previewLayer.transformedMetadataObjectForMetadataObject(object as! AVMetadataObject) as! AVMetadataMachineReadableCodeObject print(dataObject) } }
控制臺輸出結果為:
我們需要將corners的坐標值進行轉換
//MARK:- 生成需要繪制的路徑 func creatPath(corners : [Any]?) -> UIBezierPath? { guard let arr = corners else { return nil } if arr.count == 0 { return nil } var index = 0 var point = CGPoint.zero //取出數組中的一個元素 并將取出的字典轉換為CGPoint類型 point = CGPoint(dictionaryRepresentation: (corners?[index] as! CFDictionary))! index += 1 let path = UIBezierPath() path.move(to: point) while index < (corners?.count)! { //取出數組中的其他元素并將取出的字典轉換為CGPoint類型 point = CGPoint(dictionaryRepresentation: (corners?[index] as! CFDictionary))! path.addLine(to: point) index += 1 ZDLog(message: point) } path.close() return path }
轉換后控制臺的打印結果
-
得到path后描邊
//MARK:- 繪制描邊 func drawCorners(objc : AnyObject) { let metadataObject = previewLayer.transformedMetadataObject(for: objc as! AVMetadataObject) let corner = (metadataObject as! AVMetadataMachineReadableCodeObject).corners guard let path = creatPath(corners: corner) else { return } let shapeLayer = CAShapeLayer() shapeLayer.lineWidth = 5 shapeLayer.strokeColor = UIColor.green.cgColor shapeLayer.fillColor = UIColor.clear.cgColor shapeLayer.path = path.cgPath containerLayer.addSublayer(shapeLayer) } }
完成這些之后只需要在上文提到的一個解析掃描數據的代理方法中依次調用這些方法就OK了
//MARK:- 解析到掃描的數據 / 當解析到掃描的數據時會調用 且所有掃描到的數據都存于metadataObjects中 / func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) {
//清空以前的線段 clearContainerLayer() for objc in metadataObjects { drawCorners(objc: objc as AnyObject) }
}</code></pre>
注意:每次描邊之前還需要清空以前的描邊線段 否則屏幕上會出現多次描邊線段
//MARK:- 清空描邊線段 func clearContainerLayer() { if let subLayers = containerLayer.sublayers { for layer in subLayers { layer.removeFromSuperlayer() } } }
五、讀取相冊里面的二維碼
- 監聽相冊按鈕點擊
//MARK:- 相冊按鈕點擊 @IBAction func photoBtnClick(_ sender: AnyObject) { if !UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { return } let picker = UIImagePickerController() picker.sourceType = .photoLibrary picker.delegate = self present(picker, animated: true, completion: nil) } }
- 實現相關代理方法
//MARK:- 調用相冊相關 extension ZDQRCodeViewController : UINavigationControllerDelegate,UIImagePickerControllerDelegate { //MARK:- 選中一張圖片時調用 func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { //取出選中圖片 guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else { return } guard let ciImage = CIImage(image: image) else { return } //創建一個探測器 let dict = [CIDetectorAccuracy : CIDetectorAccuracyHigh] let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: dict) //利用探測器探測結果 let features = detector?.features(in: ciImage) //取出結果 for result in features! { ZDLog(message: (result as! CIQRCodeFeature).messageString) } //只要實現代理方法,就需要手動關閉瀏覽器 picker.dismiss(animated: true, completion: nil) } }
注意:需要同時遵守 UINavigationControllerDelegate 、 UIImagePickerControllerDelegate 這兩個協議
六、生成二維碼
//創建濾鏡 let filter = CIFilter(name: "CIQRCodeGenerator") //還原濾鏡 filter?.setDefaults() //設置數據 let data = "測試數據".data(using: String.Encoding.utf8) filter?.setValue(data, forKey: "inputMessage") //從濾鏡中取出數據 guard var ciImage = filter?.outputImage else { return } //設置圖片的清晰度 let transform = CGAffineTransform(scaleX: 10, y: 10) ciImage = ciImage.applying(transform) let image = UIImage(ciImage: ciImage) //設置圖片 customImageView.image = image
來自:http://www.jianshu.com/p/ae17815453ee
- 監聽相冊按鈕點擊
- 設置解析數據的區域(在output初始化時進行設置)