Swift 二維碼識(shí)別

二維碼識(shí)別是很常見(jiàn)的app功能,為了更方便的在每一個(gè)使用二維碼功能地方都能更快的實(shí)現(xiàn),把二維碼功能寫(xiě)入到了一個(gè)自定義的View里面,使用的時(shí)候和普通的UIView是一樣的。效果如圖(因?yàn)槭悄M器運(yùn)行的,所以攝像頭看不到,用真機(jī)的時(shí)候就正常了):

[圖片上傳失敗...(image-f60e5b-1527174275780)]

這篇文章只是為了快速實(shí)現(xiàn)效果,更細(xì)的知識(shí)點(diǎn),比如自定義控件中更詳細(xì)的內(nèi)容不累述。
二維碼識(shí)別分為三部實(shí)現(xiàn):

  • 自定義UIView,實(shí)現(xiàn)方形的掃描區(qū)域
  • 實(shí)現(xiàn)攝像頭捕捉
  • 掃描的橫線動(dòng)畫(huà)

自定義UIView

首先新建一個(gè)類繼承自UIView

class QRScannerView: UIView

接著實(shí)現(xiàn)兩個(gè)重要的方法:

required init(coder aDecoder: NSCoder)// 這個(gè)方法實(shí)現(xiàn)的目的是,我們?cè)趕toryboard文件中使用這個(gè)View的時(shí)候,會(huì)直接顯示出來(lái)效果。
override func drawRect(rect: CGRect)// 這個(gè)實(shí)現(xiàn)的目的是繪制我們要顯示的內(nèi)容

這里簡(jiǎn)單說(shuō)一下這個(gè)init(coder aDecoder: NSCoder),這個(gè)構(gòu)造函數(shù)不是必須的,但是為了達(dá)到跟原生控件一樣的效果:在布局的時(shí)候可以直接在布局文件中看到效果,實(shí)現(xiàn)這個(gè)構(gòu)造函數(shù)就很重要了。第二個(gè)方法是實(shí)現(xiàn)二維碼區(qū)域表現(xiàn)出來(lái)的視圖樣式的主要地方,這里可以繪制各
種圖形和樣式。

在布局中直接展示效果

兩個(gè)方法實(shí)現(xiàn)的代碼如下:

required init(coder aDecoder: NSCoder)
{
    super.init(coder: aDecoder)
    self.initView()
}
override func drawRect(rect: CGRect)
{
    let centerRect = getScannerRect(rect)
    //獲取畫(huà)圖上下文
    let context:CGContextRef = UIGraphicsGetCurrentContext();
    CGContextSetAllowsAntialiasing(context, true)
    
    // 填充整個(gè)控件區(qū)域
    CGContextSetFillColorWithColor(context, mBackgroundColor.CGColor)
    CGContextFillRect(context, rect)
    
    //移動(dòng)坐標(biāo)
    let x = rect.size.width/2
    let y = rect.size.height/2
    var center = CGPointMake(x,y)
    // 中間扣空
    CGContextClearRect(context, centerRect)
    
    // 繪制正方形框
    CGContextSetStrokeColorWithColor(context, UIColor.whiteColor().CGColor)
    CGContextSetLineWidth(context, mLineSize)
    CGContextAddRect(context, centerRect)
    CGContextDrawPath(context, kCGPathStroke)
    // 繪制4個(gè)角
    let cornerWidth = centerRect.width/mCornerLineRatio;
    let cornerHeight = centerRect.height/mCornerLineRatio;
      let cornerWidth = CGFloat(10)
      let cornerHeight = CGFloat(10)
    CGContextSetLineWidth(context, mCornerLineSize)
    CGContextSetStrokeColorWithColor(context, UIColor.greenColor().CGColor)
    // 繪制左上角
    CGContextMoveToPoint(context, centerRect.origin.x, centerRect.origin.y + cornerHeight)
    CGContextAddLineToPoint(context, centerRect.origin.x, centerRect.origin.y)
    CGContextAddLineToPoint(context, centerRect.origin.x + cornerWidth, centerRect.origin.y)
    // 繪制右上角
    CGContextMoveToPoint(context, centerRect.origin.x + centerRect.size.width - cornerWidth, centerRect.origin.y)
    CGContextAddLineToPoint(context, centerRect.origin.x + centerRect.size.width, centerRect.origin.y)
    CGContextAddLineToPoint(context, centerRect.origin.x + centerRect.size.width, centerRect.origin.y + cornerHeight)
    // 繪制右下角
    CGContextMoveToPoint(context, centerRect.origin.x + centerRect.size.width, centerRect.origin.y + centerRect.size.height - cornerHeight)
    CGContextAddLineToPoint(context, centerRect.origin.x + centerRect.size.width, centerRect.origin.y + centerRect.size.height)
    CGContextAddLineToPoint(context, centerRect.origin.x + centerRect.size.width - cornerWidth, centerRect.origin.y + centerRect.size.height)
    // 繪制左下角
    CGContextMoveToPoint(context, centerRect.origin.x, centerRect.origin.y + centerRect.size.height - cornerHeight)
    CGContextAddLineToPoint(context, centerRect.origin.x, centerRect.origin.y + centerRect.size.height)
    CGContextAddLineToPoint(context, centerRect.origin.x + cornerWidth, centerRect.origin.y + centerRect.size.height)
    CGContextDrawPath(context, kCGPathStroke)
}

現(xiàn)在可以看到init(coder aDecoder: NSCoder)這個(gè)方法初始化了一些數(shù)據(jù),這些數(shù)據(jù)同樣需要展示到布局中,所以在這里來(lái)做這件事情。
所有繪制的代碼需要在drawRect(rect: CGRect)中實(shí)現(xiàn),繪制的步驟分成了以下幾步:

  • 填充控件背景
  • 在背景中扣一個(gè)透明的洞
  • 在背景之上繪制正方形框
  • 繪制4個(gè)角

繪制的畫(huà)筆跟現(xiàn)實(shí)中一樣的,后面繪制的會(huì)覆蓋前面繪制的,如果有交集的話。

到此自定義控件的界面已經(jīng)完成,這個(gè)時(shí)候可以看到有一個(gè)方形的框在屏幕上了,具體樣式看上圖。

捕捉攝像頭數(shù)據(jù)

AVFoundation來(lái)捕捉攝像頭數(shù)據(jù),并處理二維碼解析出來(lái)的數(shù)據(jù)。


攝像頭獲取數(shù)據(jù)效果

攝像頭采集很簡(jiǎn)單,只需要使用ios提供的API就能很容易的實(shí)現(xiàn)。在這個(gè)例子中,有一個(gè)初始化方法主要用來(lái)做攝像頭數(shù)據(jù)采集的:

/**
初始化相機(jī)捕捉
**/
func initCapture(captureView:UIView, delegate:AVCaptureMetadataOutputObjectsDelegate)
{
    mCaptureView = captureView
    let captureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)
    var error: NSError?
    let input: AnyObject! = AVCaptureDeviceInput.deviceInputWithDevice(captureDevice, error: &error)
    if (error != nil)
    {
        println("\(error?.localizedDescription)")
    }
    else
    {
        let captureViewFrame = captureView.frame
        mCaptureSession = AVCaptureSession()
        mCaptureSession?.addInput(input as! AVCaptureInput)
        let captureMetadataOutput = AVCaptureMetadataOutput()
        let screenHeight = captureViewFrame.height;
        let screenWidth = captureViewFrame.width;
        let cropRect = self.frame;
        captureMetadataOutput.rectOfInterest = CGRectMake(cropRect.origin.y / screenHeight,cropRect.origin.x / screenWidth,cropRect.size.height / screenHeight,cropRect.size.width / screenWidth)
        mCaptureSession?.addOutput(captureMetadataOutput)
        captureMetadataOutput.setMetadataObjectsDelegate(delegate, queue: dispatch_get_main_queue())
        captureMetadataOutput.metadataObjectTypes = supportedBarCodes
        
        mVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: mCaptureSession)
        mVideoPreviewLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
        mVideoPreviewLayer?.frame = mCaptureView!.frame
        mCaptureView!.layer.addSublayer(mVideoPreviewLayer)
        
        addMaskLayer()
    }
}

這里這些API不光是適用于二維碼,包括條形碼等等都可以處理,主要是通過(guò)這個(gè)屬性來(lái)過(guò)濾:

captureMetadataOutput.metadataObjectTypes = supportedBarCodes

這個(gè)屬性可以有很多值:

[AVMetadataObjectTypeQRCode, 
AVMetadataObjectTypeCode128Code, 
AVMetadataObjectTypeCode39Code, 
AVMetadataObjectTypeCode93Code, 
AVMetadataObjectTypeUPCECode, 
AVMetadataObjectTypePDF417Code, 
AVMetadataObjectTypeEAN13Code, 
AVMetadataObjectTypeAztecCode]

這里主要介紹二維碼,所以只用其中的一個(gè)。

對(duì)UIView的layer層級(jí)做一個(gè)簡(jiǎn)要說(shuō)明:使用了兩個(gè)layer,一個(gè)是攝像頭捕捉的layer:videoPreviewLayer,一個(gè)是蒙板layer這個(gè)layer的作用就是在中間扣一個(gè)白色的洞,讓掃描框之外的區(qū)域看起來(lái)顏色更暗。初始化這個(gè)蒙板的代碼如下:

/**
獲取蒙板
**/
private func getMaskLayer(rect:CGRect) -> CAShapeLayer
{
    let layer = CAShapeLayer.new()
    setMaskLayer(layer, rect: rect)
    return layer
}

有了這兩個(gè)蒙板只要按照順序添加layerUIView就好了。這里要注意的一個(gè)地方就是到目前為止,如果直接在布局文件中,放入控件,比如底上的那行字,這個(gè)時(shí)候運(yùn)行,你是看不到這行字的,原因就是這行字的層級(jí)比蒙板層級(jí)要低,所以被擋住了,所以我們?cè)谔砑油昝砂搴?,我們把父控件的每一個(gè)子控件移動(dòng)到最頂層,當(dāng)然移動(dòng)的時(shí)候要排除我們這個(gè)二維碼View:

/**
把所有的其他圖層移動(dòng)到最頂層
**/
private func moveAllToFront(view:UIView)
{
    for var i = 0; i < view.subviews.count; ++i
    {
        if let view: QRScannerView = view.subviews[i] as? QRScannerView
        {
        }
        else
        {
            view.bringSubviewToFront(view.subviews[i] as! UIView)
        }
    }
}

到此攝像頭捕捉部分就完成了,下面介紹怎么添加橫線移動(dòng)的動(dòng)畫(huà)。

添加掃描線動(dòng)畫(huà)

添加動(dòng)畫(huà)代碼比較簡(jiǎn)單,就直接貼了:

/**
開(kāi)始橫線移動(dòng)
**/
func startLineRunning()
{
    let rect = self.bounds
    let lineFrame = self.mMoveLine?.frame
    UIView.animateWithDuration(1.5 ,animations: {
        self.mMoveLine?.frame.origin.y = rect.height
        }){(Bool) in
            self.mMoveLine!.frame.origin.y = 0
            self.startLineRunning()
    }
}

完成動(dòng)畫(huà)后,我們需要在啟動(dòng)攝像頭捕捉的時(shí)候讓動(dòng)畫(huà)啟動(dòng),當(dāng)然也需要可以停止:

/**
開(kāi)始捕捉視頻
**/
func startRunning()
{
    mCaptureSession?.startRunning()
    mMoveLine?.hidden = false
    if self.mLineAnimationEnable
    {
        self.mLineAnimationEnable = false
        self.startLineRunning()
    }
}
/**
停止捕捉視頻
**/
func stopRunning()
{
    mMoveLine?.hidden = true
    mCaptureSession?.stopRunning()
}

實(shí)例代碼:

實(shí)現(xiàn)好了QRScannerView后怎么使用呢?

  • 在controller實(shí)現(xiàn)協(xié)議:
AVCaptureMetadataOutputObjectsDelegate
  • 其他代碼包含啟動(dòng)、初始化、和攝像頭捕捉的數(shù)據(jù)處理,這里主要是func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!)會(huì)獲取到捕捉到的二維碼數(shù)據(jù):
override func viewDidAppear(animated: Bool)
    {
        scanner.startRunning()
        isCaputure = false
    }
    override func viewDidDisappear(animated: Bool)
    {
        scanner.stopRunning()
    }
    override func viewWillAppear(animated: Bool)
    {
        scanner.initCapture(self.view, delegate: self)
    }
    var isCaputure = false
    /**
    捕捉回調(diào)
    **/
    func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!)
    {
        var resultString = ""
        if metadataObjects == nil || metadataObjects.count == 0
        {
            resultString = "scanner error"
        }
        else
        {
            let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject
            if self.scanner.supportedBarCodes.filter({ $0 == metadataObj.type }).count > 0
            {
                let barCodeObject = self.scanner.videoPreviewLayer?.transformedMetadataObjectForMetadataObject(metadataObj as AVMetadataMachineReadableCodeObject) as! AVMetadataMachineReadableCodeObject
                resultString = barCodeObject.stringValue
            }
        }
        print("isCaputure    \(isCaputure)")
        if !isCaputure
        {
            isCaputure = true
            self.requestValidate(resultString)
        }
    }

完整demo下載地址:DemoQRView

總結(jié)

要實(shí)現(xiàn)任何一個(gè)自定義控件也好其他app功能也好,永遠(yuǎn)都是數(shù)據(jù)和界面分離的思維,界面什么樣子數(shù)據(jù)管不著,數(shù)據(jù)什么樣子界面管不著,至于說(shuō)聯(lián)系起來(lái)的方式就很多了,常用的一種就是界面需要什么樣子的數(shù)據(jù),數(shù)據(jù)就怎么提供,還可以在中間添加適配器,不管什么數(shù)據(jù)都轉(zhuǎn)化成界面需要的數(shù)據(jù)結(jié)構(gòu)。

??查看更多??

不登高山,不知天之高也;不臨深溪,不知地之厚也
感謝指點(diǎn)、交流、喜歡

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容