Swift中二維碼的相關操作

二維碼

  • 二維碼概念
    • 二維碼是用某種特點的集合圖形按一定規律在平面(二維方向上)分布的黑白相間的圖形,用于記錄數據符號信息;
  • 二維碼的使用場景
    • 信息獲取(名片,WIFI密碼,資料)
    • 手機電商(用戶掃碼,手機直接購物下單)
    • 加好友(QQ,微信,掃一掃加好友)
    • 手機支付(掃描商品二維碼,永固歐銀行或第三方支付提供的手機端通道完成支付)
  • 二維碼的生成方式
    • 從iOS7開始繼承了二維碼的生成和讀取功能
    • 此前被廣泛使用的zbars和zxing目前不支持64位處理器(5s以后都是64位處理器)(2015年2月1號開始,不允許不支持64位處理器的APP上架)

生成二維碼

  • 步驟
    1. 獲取文本框輸入的內容
    2. 生成一個二維碼濾鏡(不同的字符串生成不同的濾鏡)
    3. 設置二維碼的內容
    4. 從二維碼濾鏡中獲取生成的二維碼
    5. 展示圖片(注意:默認生成的二維碼大小為23,放大圖片后就會顯得模糊)

生成二維碼實例

/// 生成二維碼
extension QRCodeTool {
    /// 生成二維碼
    ///
    /// - Parameters:
    ///   - contentStr: 二維碼內容
    ///   - bigImageWH: 二維碼大小
    ///   - smallImage: 小圖標
    ///   - smallImageSize: 小圖標大小
    /// - Returns: 生成一個自頂一個二維碼圖片返回
    class func gerneratorQRCode(contentStr : String, bigImageWH :  CGFloat, smallImage : UIImage, smallImageSize : CGFloat) -> UIImage? {
        // 1.創建一個二維碼濾鏡
        guard let filter = CIFilter(name: "CIQRCodeGenerator") else {return nil}
        
        // 1.1恢復默認值
        filter.setDefaults()
        
        // 2.設置內容
        // 注意點
        // 如果要設置二維碼內容,必須以KVC的方式設置
        // 二維碼的值必須是NSData
        let data = contentStr.data(using: .utf8)
        filter.setValue(data, forKeyPath: "inputMessage")
        
        // 2.1設置二維碼的級別(糾錯率)
        //        key : inputCorrectionLevel
        //        value L: 7% M(默認  ): 15% Q: 25% H: 30%
        filter.setValue("H", forKeyPath: "inputCorrectionLevel")
        
        // 3.從二維碼濾鏡中獲取二維碼
        guard let outputImage = filter.outputImage else {return nil}
        
        // 4.展示圖片(生成的二維碼圖片的大小為23*23,放大到200*200就會變得很模糊)
        //        let imageUI = UIImage(ciImage: outputImage)
        //        print(imageUI.size)
        guard let imageUI = createClearImage(outputImage, size: bigImageWH) else {return nil}
        
        // 4.設置二維碼顏色(注意:設置二維碼顏色,必須放在清晰的二維碼之后,如果先設置顏色,再改變清晰度,則設置顏色無效)
        let imageCI = CIImage(image: imageUI)
        let colorFiler = CIFilter(name: "CIFalseColor")
        colorFiler?.setDefaults()
        // 設置圖片
        colorFiler?.setValue(imageCI, forKeyPath: "inputImage")
        // 設置二維碼亞瑟
        colorFiler?.setValue(CIColor.init(color: UIColor.yellow), forKeyPath: "inputColor0")
        // 設置背景顏色
        colorFiler?.setValue(CIColor.init(color: UIColor.red), forKeyPath: "inputColor1")
        guard let colorOutputImage = colorFiler?.outputImage else {return nil}
        let image  = UIImage(ciImage: colorOutputImage)
        return createCustomImage(bigImage: image, smallImage: smallImage, smallImageWH: smallImageSize)
    }

    /// 自定義二維碼
    ///
    /// - Parameters:
    ///   - bigImage: 二維碼圖片
    ///   - smallImage: 小圖片
    ///   - smallImageWH: 小圖片尺寸
    /// - Returns: 生成新的二維碼圖片
    fileprivate class func createCustomImage(bigImage : UIImage, smallImage : UIImage, smallImageWH : CGFloat) -> UIImage? {
        
        // 0.大圖片的尺寸
        let bigImageSize = bigImage.size
        
        // 1.創建圖形上下文
        UIGraphicsBeginImageContext(bigImageSize)
        
        // 2.繪制大圖片
        bigImage.draw(in: CGRect(x: 0, y: 0, width: bigImageSize.width, height: bigImageSize.height))
        
        // 3.繪制小圖片
        smallImage.draw(in: CGRect(x: (bigImageSize.width - smallImageWH) * 0.5, y: (bigImageSize.height - smallImageWH) * 0.5, width: smallImageWH, height: smallImageWH))
        
        // 4.從圖形上下文中取出圖片
        let image = UIGraphicsGetImageFromCurrentImageContext()
        
        // 5.關閉圖形上下文
        UIGraphicsEndImageContext()
        
        // 6.返回圖片
        return image
        
    }
    
    /// 將模糊的二維碼圖片轉為清晰的
    ///
    /// - parameter image: 模糊的二維碼圖片
    /// - parameter size: 需要生成的二維碼尺寸
    ///
    /// - returns: 清晰的二維碼圖片
    fileprivate class func createClearImage(_ image : CIImage, size : CGFloat ) -> UIImage? {
        
        // 1.調整小數像素到整數像素,將origin下調(12.*->12),size上調(11.*->12)
        let extent = image.extent.integral
        
        // 2.將指定的大小與寬度和高度進行對比,獲取最小的比值
        let scale = min(size / extent.width, size/extent.height)
        
        // 3.將圖片放大到指定比例
        let width = extent.width * scale
        let height = extent.height * scale
        
        // 3.1創建依賴于設備的灰度顏色通道
        let cs = CGColorSpaceCreateDeviceGray();
        
        // 3.2創建位圖上下文
        let bitmapRef = CGContext(data: nil, width: Int(width), height: Int(height), bitsPerComponent: 8, bytesPerRow: 0, space: cs, bitmapInfo: 0)
        
        // 4.創建上下文
        let context = CIContext(options: nil)
        
        // 5.將CIImage轉為CGImage
        let bitmapImage = context.createCGImage(image, from: extent)
        
        // 6.設置上下文渲染等級
        bitmapRef!.interpolationQuality = .none
        
        // 7.改變上下文的縮放
        bitmapRef?.scaleBy(x: scale, y: scale)
        
        // 8.繪制一張圖片在位圖上下文中
        bitmapRef?.draw(bitmapImage!, in: extent)
        
        // 9.從位圖上下文中取出圖片(CGImage)
        guard let scaledImage = bitmapRef?.makeImage() else {return nil}
        
        // 10.將CGImage轉為UIImage并返回
        return UIImage(cgImage: scaledImage)
    }
}

效果

識別二維碼

  • 步驟
    1. 創建二維碼探測器
    2. 探測圖片特征
    3. 讀取圖片特征

識別二維碼實例

extension QRCodeTool {
    /// 識別圖片中的二維碼
    ///
    /// - Parameter sourceImage: 原始圖片
    /// - Returns: 將識別到的二維碼繪制邊框并返回圖片
    class func detectorQRCode(sourceImage : UIImage) -> UIImage? {
        // 0.創建上下文
        let context = CIContext()
        
        // 1.創建二維碼的探測器
        guard let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: context, options: [CIDetectorAccuracy : CIDetectorAccuracyHigh]) else {return nil}
        
        // 2.探測圖片
        guard let sourceImageCI = CIImage(image: sourceImage) else {return nil}
        guard let features = detector.features(in: sourceImageCI) as? [CIQRCodeFeature] else {return nil}
        
        // 3.繪制識別的二維碼的邊框
        return drawQRCodeBorder(features: features, sourceImage: sourceImage)
    }
    
    fileprivate class func drawQRCodeBorder(features : [CIQRCodeFeature], sourceImage : UIImage) -> UIImage? {
        
        // 0.源圖片大小
        let sourceImageSize = sourceImage.size
        
        // 1.創建圖形上下文
        UIGraphicsBeginImageContext(sourceImageSize)
        
        // 2.繪制圖片
        sourceImage.draw(in: CGRect(x: 0, y: 0, width: sourceImageSize.width, height: sourceImageSize.height))
        
        // 3.繪制邊框(bounds的坐標原點在左下角)
        // 第二種解決方式
        // 獲取圖形上下文
        let context = UIGraphicsGetCurrentContext()
        
        // 改變圖形上下文的坐標原點
        // 注意:必須放在繪制之前
        context?.scaleBy(x: 1, y: -1)
        context?.translateBy(x: 0, y: -sourceImageSize.height)
        for feature in features {
            let bounds = feature.bounds
            // 第一種解決方式
            // let newBounds = CGRect(x: bounds.origin.x, y:sourceImageSize.height - bounds.origin.y - bounds.height , width: bounds.width, height: bounds.height)
            
            // let path = UIBezierPath(rect: newBounds)
            let path = UIBezierPath(rect: bounds)
            UIColor.red.set()
            path.lineWidth = 4
            path.stroke()
        }
        
        // 4.取出圖片
        let image = UIGraphicsGetImageFromCurrentImageContext()
        
        // 5.關閉圖形上下文
        UIGraphicsEndImageContext()
        
        // 6.返回圖片
        return image
    }
}

效果

掃描二維碼

  • 掃描功能
    1. 功能描述
      • 使用攝像頭掃描圖片中的二維碼,獲取二維碼內的數據
    2. 掃描二維碼原理
      1. 需要使用AVFoundation
      2. 獲取攝像頭,作為輸入設備
      3. 創建輸出對象,并設置代理,掃描到結果后就會通過代理告訴外界
      4. 創建捕捉會話,將輸入和輸出連接起來
      5. 開啟會話,執行掃描操作
    3. 注意點
      1. 訪問用戶的攝像頭需要請求授權
        • info.plist配置Privacy - Camera Usage Description
      2. 在使用輸出設備設置代理以及隊列時注意傳的是主隊列還是全局隊列
      3. 使用會話連接輸入和輸出之前先判斷是否可以添加
      4. 必須制定掃描的類型,并在將輸出添加到會話后再添加,否則會崩潰
      5. 保證捕捉會話不被銷毀
      6. 當掃描結果時會不斷的調用代理方法
class QRCodeTool: NSObject {
    /// 掃描二維碼
    
    // 單例對象
    static let shareInstance = QRCodeTool()
    
    ///懶加載
    /// 創建會話(順便添加輸入和輸出對象)
    fileprivate lazy var session : AVCaptureSession? = {
        // 1.獲取攝像頭設備
        guard let captureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) else {return nil}
        
        // 2.將攝像頭作為輸入對象
        var input : AVCaptureDeviceInput?
        do {
            input = try AVCaptureDeviceInput(device: captureDevice)
            
        } catch {
            print(error)
            return nil
        }
        
        // 3.創建輸出對象
        let output = AVCaptureMetadataOutput()
        output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
        
        // 4.創建捕捉會話
        let session = AVCaptureSession()
        
        // 5.添加輸入對象和輸出對象到會話中
        if session.canAddInput(input!) && session.canAddOutput(output)  {
            session.addInput(input!)
            session.addOutput(output)
            
            // 注意:如果要掃描任何碼制,必須制定掃描的類型
            // 必須在添加了輸出對象之后再設置
            output.metadataObjectTypes = [AVMetadataObjectTypeQRCode]
        }
        return session
    }()
    
    /// 創建預覽圖層
    /// 將預覽圖層添加到layer上,注意添加到最底層,否則會遮住當前的控件
    fileprivate lazy var preViewLayer : AVCaptureVideoPreviewLayer? = {
        guard let preViewLayer = AVCaptureVideoPreviewLayer(session: self.session) else {return nil}
        return preViewLayer
    }()
    
    // 定義閉包
    typealias ScanResultBlock = (_ resultStr : String) -> ()
    
    var scanResultBlock : ScanResultBlock?
    
    func scanQRCode(scanView : UIView, preView : UIView, scanResultBlock : @escaping ScanResultBlock) -> () {
        
        // 0.記錄block
        self.scanResultBlock = scanResultBlock
        
        // 1.判斷會話和預覽圖層是否有值
        if session == nil || preViewLayer == nil {
            return
        }
        
        if let subLayers = preView.layer.sublayers {
            let isHavePreViewLayer = subLayers.contains(preViewLayer!)
            if !isHavePreViewLayer {
                preView.layer.insertSublayer(preViewLayer!, at: 0)
                preViewLayer?.frame = preView.bounds
                
                guard let output = session!.outputs.first as? AVCaptureMetadataOutput  else {
                    return
                }
                // 設置掃描區域(內部會以坐標原點在右上角設置)
                let x = scanView.frame.origin.y / preView.bounds.height
                let y = scanView.frame.origin.x / preView.bounds.width
                let w = scanView.bounds.height / preView.bounds.height
                let h = scanView.bounds.width / preView.bounds.width
                output.rectOfInterest = CGRect(x: x, y: y, width: w, height: h)
            }
        }
        // 2.判斷是否正在掃描
        if session!.isRunning {
            return
        }
        // 3.開始掃描
        session!.startRunning()
    }
    
    
    /// 控制手機手電筒
    /// 步驟 
    /// 1.需要使用AVFoundation框架
    /// 2.獲取手機攝像頭
    /// 3.通過攝像頭獲取硬件的控制權
    /// 4.設置手電筒的模式
    /// 5.取消硬件的控制權
    class func turnLight(isOn : Bool) {
        
        // 1.獲取攝像頭
        guard let captureDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo) else {return}
        
        // 2.獲取設備的控制權
        do {
            try captureDevice.lockForConfiguration()
        } catch {
            print(error)
            return
        }
        
        if isOn { // 打開手電筒
            captureDevice.torchMode = .on
            
        } else { // 關閉手電筒
            captureDevice.torchMode = .off
        }
        
        // 3.釋放設備的控制權
        captureDevice.unlockForConfiguration()
    }
}

/// AVCaptureMetadataOutputObjectsDelegate
extension QRCodeTool : AVCaptureMetadataOutputObjectsDelegate {
    
    /// 當掃描到結果的時候就會來到該方法(任何碼制都會來到該方法)
    ///
    /// - Parameters:
    ///   - captureOutput: 輸出
    ///   - metadataObjects: 掃描到碼制數組
    ///   - connection: 鏈接
    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) {
        
        removeQRCodeBorder()
        
        // 掃描的類型AVMetadataMachineReadableCodeObject(二維碼類型)
        // 這里只處理二維碼
        // 當移除屏幕的時候,系統會額外調用一次該方法,內容是空
        
        guard let result = metadataObjects.first as? AVMetadataMachineReadableCodeObject else {return}
        scanResultBlock!(result.stringValue)
        drawQRCodeBorder(result: result)
        
//        guard let results = metadataObjects as? [AVMetadataMachineReadableCodeObject] else {return}
//        for result in results {
//            //            print(result.stringValue, result.corners)
//            drawQRCodeBorder(result: result)
//        }
        
    }
    
    /// 給二維碼繪制邊框
    ///
    /// - Parameter result: 掃描到的二維碼結果
    private func drawQRCodeBorder(result : AVMetadataMachineReadableCodeObject) {
        
        // result.corners:是數據坐標,必須使用預覽圖層將坐標轉為位(上下文)的坐標
        guard let resultObj = preViewLayer?.transformedMetadataObject(for: result) as? AVMetadataMachineReadableCodeObject else {return}
        // 繪制邊框
        let path = UIBezierPath()
        var index = 0
        for corner in resultObj.corners {
            
            let dictCF = corner as! CFDictionary
            let point = CGPoint(dictionaryRepresentation: dictCF)!
            
            // 開始繪制
            if index == 0 {
                path.move(to: point)
            } else {
                path.addLine(to: point)
            }
            
            index = index + 1
        }
        
        // 關閉路徑
        path.close()
        
        // 創建形狀圖層
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = path.cgPath
        shapeLayer.lineWidth = 3
        shapeLayer.strokeColor = UIColor.red.cgColor
        shapeLayer.fillColor = UIColor.clear.cgColor
        preViewLayer?.addSublayer(shapeLayer)
    }
    
    /// 移除邊框
    private func removeQRCodeBorder() {
        
        if let layers = preViewLayer?.sublayers {
            for layer in layers {
                let shapeLayer = layer as? CAShapeLayer
                if shapeLayer != nil {
                    shapeLayer?.removeFromSuperlayer()
                }
            }
        }   
    }
}

效果

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容