iOS开发之二维码扫描以及生成
vera
8年前
<h2><strong>简介</strong></h2> <ul> <li>二维条码/二维码是用某种 特定的几何图形 按一定规律在平面分布的黑白相间的图形记录数据符号信息的</li> <li>在编码上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息</li> <li>通过图象输入设备或光电扫描设备自动识读以实现信息自动处理</li> </ul> <h3><strong>特点</strong></h3> <ul> <li>每种码制有其特定的字符集</li> <li>每个字符占有一定的宽度</li> <li>具有一定的校验功能</li> </ul> <h3><strong>功能</strong></h3> <ul> <li>信息获取(名片、地图、WIFI密码、资料)</li> <li>网站跳转(跳转到微博、手机网站、网站)</li> <li>广告推送(用户扫码,直接浏览商家推送的视频、音频广告)</li> <li>手机电商(用户扫码、手机直接购物下单)</li> <li>防伪溯源(用户扫码、即可查看生产地;同时后台可以获取最终消费地)</li> <li>优惠促销(用户扫码,下载电子优惠券,抽奖)</li> <li>会员管理(用户手机上获取电子会员信息、VIP服务)</li> <li>手机支付(扫描商品二维码,通过银行或第三方支付提供的手机端通道完成支付)</li> </ul> <p>目前越来越多的app使用了二维码,且二维码传播更为快捷,方便。</p> <p>iOS开发中也有诸如 ZBar 、 ZXing 这样优秀的第三方框架,但是其不支持64位。今天就带大家一起来自己动手写一下二维码扫描以及二维码生成的这一功能。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/4d02a456e5fc85cd22c1f139faaaa553.gif"></p> <p>就以新浪微博的二维码扫描界面为例来进行讲解。</p> <h2><strong>一、扫描界面动画</strong></h2> <p>界面这一块我是以storyboard来进行搭建的,想将更多的精力放在二维码扫描以及生成上。</p> <p><img src="https://simg.open-open.com/show/2a1b6c887a750b3c946141446a0bbf0d.jpg"></p> <ul> <li>用 navigationController 包装一个最普通的 viewController</li> <li>然后再在 viewController 上依次加入如图所示的3个子控件<br> 界面基本搭建完成之后就要拖控件以及约束到对应控制器中来做扫描冲击波的动画了。<br> 在 ZDQRCodeViewController.swift 中主要会对容器视图的高度约束、冲击波图片顶部约束进行相应设置来进行动画 <pre> <code class="language-objectivec">//冲击波顶部约束 @IBOutlet weak var scanLineTopConstraint: NSLayoutConstraint! //容器视图高度约束 @IBOutlet weak var containerViewHeightConstraint: NSLayoutConstraint!</code></pre> </li> <li>首先设置冲击波的顶部约束的 constant 等于负的容器视图的高度约束的 constant ,然后再在一定时间内让其等于正的容器视图的高度约束的 constant 如此无限重复就会展现出想要的动画效果。 <pre> <code class="language-objectivec">//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() } } }</code></pre> 注意:要将容器视图的 Clip To Bounds 勾上否则动画看起来不流畅 <p>还有一点需要特别注意的:上面定义的 startAnimation() 这个方法一定要在 func viewWillAppear(_ animated: Bool) 这个方法里面调用 否则看不到动画效果</p> </li> </ul> <h2><strong>二、扫描二维码</strong></h2> <p>识别原理</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/76cddc44988edd64cc93e950b052b46f.jpg"></p> <p>输入设备与输出数据是通过拍摄会话 AVCaptureSession 来进行沟通的。</p> <p>代码实现</p> <ul> <li> <p>摄像头输入设备</p> <pre> <code class="language-objectivec">//输入设备 lazy var inputDevice : AVCaptureDeviceInput? = { let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) return try? AVCaptureDeviceInput(device: device) }()</code></pre> </li> <li> <p>拍摄会话</p> <pre> <code class="language-objectivec">lazy var session = AVCaptureSession()</code></pre> </li> <li>数据输出 <pre> <code class="language-objectivec">lazy var output = AVCaptureMetadataOutput()</code></pre> </li> <li>设置预览图层 <pre> <code class="language-objectivec">lazy var previewLayer : AVCaptureVideoPreviewLayer = { let layer = AVCaptureVideoPreviewLayer(session: self.session) return layer! }()</code></pre> </li> <li>建立通道、设置会话 <pre> <code class="language-objectivec">//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() }</code></pre> </li> <li> <p>实现代理方法</p> <pre> <code class="language-objectivec">func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) { print(metadataObjects) }</code></pre> </li> </ul> <h2><strong>三、设置扫描区域</strong></h2> <p>为了能够拥有更好的用户体验,我们应该需要设置一下扫描的区域,让用户将二维码放进界面中的扫描区域内再开始扫描</p> <ul> <li>设置解析数据的区域(在output初始化时进行设置) <pre> <code class="language-objectivec">//输出对象 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 }()</code></pre> <p>注意:苹果默认为扫描二维码时手机是处于横屏的,所以计算比例时要将X,Y,W,H反过来 要像我上面代码中的计算方式</p> </li> </ul> <h2><strong>四、设置描边</strong></h2> <ul> <li> <p>坐标转换</p> <pre> <code class="language-objectivec">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) } }</code></pre> <p>控制台输出结果为:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/fe86a5d6d556551b0b69ee9033f698cf.jpg"></p> <p>我们需要将corners的坐标值进行转换</p> <pre> <code class="language-objectivec">//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 }</code></pre> <p>转换后控制台的打印结果</p> </li> </ul> <p style="text-align:center"><img src="https://simg.open-open.com/show/32628e0dea414c8100309eff365f6a31.jpg"></p> <ul> <li> <p>得到path后描边</p> <pre> <code class="language-objectivec">//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) } }</code></pre> <p>完成这些之后只需要在上文提到的一个解析扫描数据的代理方法中依次调用这些方法就OK了</p> <pre> <code class="language-objectivec">//MARK:- 解析到扫描的数据 /* ** 当解析到扫描的数据时会调用 ** 且所有扫描到的数据都存于metadataObjects中 */ func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { //清空以前的线段 clearContainerLayer() for objc in metadataObjects { drawCorners(objc: objc as AnyObject) } }</code></pre> <p>注意:每次描边之前还需要清空以前的描边线段 否则屏幕上会出现多次描边线段</p> <pre> <code class="language-objectivec">//MARK:- 清空描边线段 func clearContainerLayer() { if let subLayers = containerLayer.sublayers { for layer in subLayers { layer.removeFromSuperlayer() } } }</code></pre> </li> </ul> <h2><strong>五、读取相册里面的二维码</strong></h2> <ul> <li>监听相册按钮点击 <pre> <code class="language-objectivec">//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) } }</code></pre> </li> <li>实现相关代理方法 <pre> <code class="language-objectivec">//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) } }</code></pre> <p>注意:需要同时遵守 UINavigationControllerDelegate 、 UIImagePickerControllerDelegate 这两个协议</p> </li> </ul> <h2><strong>六、生成二维码</strong></h2> <pre> <code class="language-objectivec">//创建滤镜 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</code></pre> <p> </p> <p>来自:http://www.jianshu.com/p/ae17815453ee</p> <p> </p>