iOS开发之二维码扫描以及生成

简介

  • 二维条码/二维码是用某种特定的几何图形按一定规律在平面分布的黑白相间的图形记录数据符号信息的
  • 在编码上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息
  • 通过图象输入设备或光电扫描设备自动识读以实现信息自动处理

特点

  • 每种码制有其特定的字符集
  • 每个字符占有一定的宽度
  • 具有一定的校验功能

功能

  • 信息获取(名片、地图、WIFI密码、资料)
  • 网站跳转(跳转到微博、手机网站、网站)
  • 广告推送(用户扫码,直接浏览商家推送的视频、音频广告)
  • 手机电商(用户扫码、手机直接购物下单)
  • 防伪溯源(用户扫码、即可查看生产地;同时后台可以获取最终消费地)
  • 优惠促销(用户扫码,下载电子优惠券,抽奖)
  • 会员管理(用户手机上获取电子会员信息、VIP服务)
  • 手机支付(扫描商品二维码,通过银行或第三方支付提供的手机端通道完成支付)

目前越来越多的app使用了二维码,且二维码传播更为快捷,方便。
iOS开发中也有诸如ZBarZXing这样优秀的第三方框架,但是其不支持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()
        }
    }
}

注意:要将容器视图的Clip To Bounds勾上否则动画看起来不流畅

还有一点需要特别注意的:上面定义的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)
}

三、设置扫描区域

为了能够拥有更好的用户体验,我们应该需要设置一下扫描的区域,让用户将二维码放进界面中的扫描区域内再开始扫描

  • 设置解析数据的区域(在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)
        }
    }

注意:每次描边之前还需要清空以前的描边线段 否则屏幕上会出现多次描边线段

//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)
    }
}

注意:需要同时遵守UINavigationControllerDelegateUIImagePickerControllerDelegate这两个协议


六、生成二维码

 //创建滤镜
        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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,513评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,312评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,124评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,529评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,937评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,913评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,084评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,816评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,593评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,788评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,267评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,601评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,265评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,158评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,953评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,066评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,852评论 2 277

推荐阅读更多精彩内容