Swift 二維碼識別
二維碼識別是很常見的app功能,為了更方便的在每一個使用二維碼功能地方都能更快的實現,把二維碼功能寫入到了一個自定義的 View 里面,使用的時候和普通的 UIView 是一樣的。效果如圖(因為是模擬器運行的,所以攝像頭看不到,用真機的時候就正常了):

二維碼效果圖
這篇文章只是為了快速實現效果,更細的知識點,比如自定義控件中更詳細的內容不累述。
二維碼識別分為三部實現:
- 自定義 UIView ,實現方形的掃描區域
- 實現攝像頭捕捉
- 掃描的橫線動畫
自定義UIView
首先新建一個類繼承自 UIView
class QRScannerView: UIView
接著實現兩個重要的方法:
required init(coder aDecoder: NSCoder)// 這個方法實現的目的是,我們在storyboard文件中使用這個View的時候,會直接顯示出來效果。
override func drawRect(rect: CGRect)// 這個實現的目的是繪制我們要顯示的內容
這里簡單說一下這個 init(coder aDecoder: NSCoder) ,這個構造函數不是必須的,但是為了達到跟原生控件一樣的效果:在布局的時候可以直接在布局文件中看到效果,實現這個構造函數就很重要了。第二個方法是實現二維碼區域表現出來的視圖樣式的主要地方,這里可以繪制各
種圖形和樣式。

在布局中直接展示效果
兩個方法實現的代碼如下:
required init(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.initView()
}
override func drawRect(rect: CGRect)
{
let centerRect = getScannerRect(rect)
//獲取畫圖上下文
let context:CGContextRef = UIGraphicsGetCurrentContext();
CGContextSetAllowsAntialiasing(context, true)
// 填充整個控件區域
CGContextSetFillColorWithColor(context, mBackgroundColor.CGColor)
CGContextFillRect(context, rect)
//移動坐標
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個角
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)
}
現在可以看到 init(coder aDecoder: NSCoder) 這個方法初始化了一些數據,這些數據同樣需要展示到布局中,所以在這里來做這件事情。
所有繪制的代碼需要在 drawRect(rect: CGRect) 中實現,繪制的步驟分成了以下幾步:
- 填充控件背景
- 在背景中扣一個透明的洞
- 在背景之上繪制正方形框
- 繪制4個角
繪制的畫筆跟現實中一樣的,后面繪制的會覆蓋前面繪制的,如果有交集的話。
到此自定義控件的界面已經完成,這個時候可以看到有一個方形的框在屏幕上了,具體樣式看上圖。
捕捉攝像頭數據
AVFoundation來捕捉攝像頭數據,并處理二維碼解析出來的數據。

攝像頭獲取數據效果
攝像頭采集很簡單,只需要使用ios提供的API就能很容易的實現。在這個例子中,有一個初始化方法主要用來做攝像頭數據采集的:
/**
初始化相機捕捉
**/
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不光是適用于二維碼,包括條形碼等等都可以處理,主要是通過這個屬性來過濾:
captureMetadataOutput.metadataObjectTypes = supportedBarCodes
這個屬性可以有很多值:
[AVMetadataObjectTypeQRCode,
AVMetadataObjectTypeCode128Code,
AVMetadataObjectTypeCode39Code,
AVMetadataObjectTypeCode93Code,
AVMetadataObjectTypeUPCECode,
AVMetadataObjectTypePDF417Code,
AVMetadataObjectTypeEAN13Code,
AVMetadataObjectTypeAztecCode]
這里主要介紹二維碼,所以只用其中的一個。
對UIView的layer層級做一個簡要說明:使用了兩個layer,一個是攝像頭捕捉的layer: videoPreviewLayer ,一個是蒙板layer這個layer的作用就是在中間扣一個白色的洞,讓掃描框之外的區域看起來顏色更暗。初始化這個蒙板的代碼如下:
/**
獲取蒙板
**/
private func getMaskLayer(rect:CGRect) -> CAShapeLayer
{
let layer = CAShapeLayer.new()
setMaskLayer(layer, rect: rect)
return layer
}
有了這兩個蒙板只要按照順序添加 layer 到 UIView 就好了。這里要注意的一個地方就是到目前為止,如果直接在布局文件中,放入控件,比如底上的那行字,這個時候運行,你是看不到這行字的,原因就是這行字的層級比蒙板層級要低,所以被擋住了,所以我們在添加完蒙板后,我們把父控件的每一個子控件移動到最頂層,當然移動的時候要排除我們這個二維碼View:
/**
把所有的其他圖層移動到最頂層
**/
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)
}
}
}
到此攝像頭捕捉部分就完成了,下面介紹怎么添加橫線移動的動畫。
添加掃描線動畫
添加動畫代碼比較簡單,就直接貼了:
/**
開始橫線移動
**/
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()
}
}
完成動畫后,我們需要在啟動攝像頭捕捉的時候讓動畫啟動,當然也需要可以停止:
/**
開始捕捉視頻
**/
func startRunning()
{
mCaptureSession?.startRunning()
mMoveLine?.hidden = false
if self.mLineAnimationEnable
{
self.mLineAnimationEnable = false
self.startLineRunning()
}
}
/**
停止捕捉視頻
**/
func stopRunning()
{
mMoveLine?.hidden = true
mCaptureSession?.stopRunning()
}
實例代碼:
實現好了 QRScannerView 后怎么使用呢?
- 在controller實現協議:
AVCaptureMetadataOutputObjectsDelegate - 其他代碼包含啟動、初始化、和攝像頭捕捉的數據處理,這里主要是 func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!) 會獲取到捕捉到的二維碼數據:
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 /** 捕捉回調 **/ 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) } }
總結
要實現任何一個自定義控件也好其他app功能也好,永遠都是數據和界面分離的思維,界面什么樣子數據管不著,數據什么樣子界面管不著,至于說聯系起來的方式就很多了,常用的一種就是界面需要什么樣子的數據,數據就怎么提供,還可以在中間添加適配器,不管什么數據都轉化成界面需要的數據結構。
來自:http://www.jianshu.com/p/b0865b82a4dc