项目中有个地方需要实现扫二维码扫描的功能。在iOS 7以后,AVFoundation
这个framework已经提供了相机扫描识别二维码的功能,所以就自己研究一下。
0x01
首先,想要获取用户的相机权限的话,需要在Info.plist中添加NSCameraUsageDescription
这个key,value的值用于展示给用户告之使用相机的用途。如果没有添加上这个key的话,就会出现下面这样的错误:
[access] This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data.
复制代码
0x02 设定扫描范围的两种方法
要设定扫描范围的话,可以通过设置captureMetadataOutput
的rectOfInterest
。
因为AVCapture的坐标系是横屏的,所以其坐标系跟window
的坐标系是x轴和y轴相反的。所以在设置的时候需要翻转坐标系,其中一种方法就是自己交换x轴和y轴的值。另外有一点需要注意的是**rectOfInterest
**的类型是CGRect
,这个需要区别于我们平常使用的CGRect
,因为rectOfInterest
的值是比例值。
第一种复制方式:
CGFloat width = self.view.frame.size.width * 3/5;
CGFloat height = width;
CGFloat x = self.view.frame.size.width / 5;
CGFloat y = (self.view.frame.size.height - height) / 2;
CGRect frame = CGRectMake(x, y, width, height); captureMetadataOutput.rectOfInterest = CGRectMake(frame.origin.y / self.view.frame.size.height,
frame.origin.x / self.view.frame.size.width,
frame.size.height / self.view.frame.size.height,
frame.size.width / self.view.frame.size.width);
复制代码
从上面可以看到,原本应该为x值的地方变成了y,y变成了x,交换了x轴和y轴的值。所有值都是比例值。
第二种赋值方式:
CGFloat width = self.view.frame.size.width * 3/5;
CGFloat height = width;
CGFloat x = self.view.frame.size.width / 5;
CGFloat y = (self.view.frame.size.height - height) / 2;
CGRect frame = CGRectMake(x, y, width, height);
self.captureMetadataOutput.rectOfInterest = [self.previewLayer metadataOutputRectOfInterestForRect:frame];
复制代码
这一种方式的话,调用了AVCaptureVideoPreviewLayer
的metadataOutputRectOfInterestForRect:
方法。该方法直接将previewLayer
的坐标系转换为output
的坐标系,也就省去了我们手动转换的步骤。
**第二种方式需要在[session startRunning];
之后才执行。**如果在running之前进行调用metadataOutputRectOfInterestForRect:
的话,会出现GAffineTransformInvert: singular matrix.
这样的错误。谷歌了一下,说是坐标系为CGRectZero.所以猜测在running之前,previewLayer并未执行,所以转换出来的结果出现了error。
0x03 一般流程
- 获取输入设备
// Find the input for default capture device
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error = nil;
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
if (!input) {
NSLog(@"Failed to init capture input: %@", [error localizedDescription]);
return;
}
复制代码
- 添加输出
// AVCaptureMetadataOutput对象用于由相关连接发出的元数据对象并将其转发到delegate进行处理
self.captureMetadataOutput = [[AVCaptureMetadataOutput alloc] init];
复制代码
- 初始化session对象并设置输入输出
self.captureSession = [[AVCaptureSession alloc] init];[self.captureSession addInput:input];
[self.captureSession addOutput:self.captureMetadataOutput];
复制代码
- 设置metadata输出
dispatch_queue_t dispatchQueue;
dispatchQueue = dispatch_queue_create("Scanner Queue", nil);
[self.captureMetadataOutput setMetadataObjectsDelegate:self queue:dispatchQueue];
[self.captureMetadataOutput setMetadataObjectTypes:@[AVMetadataObjectTypeQRCode]] // 设置扫描类型
复制代码
- 添加预览层
self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession];
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
self.previewLayer.frame = self.view.bounds;
复制代码
- 开启扫描
[self.captureSession startRunning];
复制代码
- 实现
AVCaptureMetadataOutputObjectsDelegate
方法
#pragma mark - AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
NSLog(@"%@", metadataObjects);
}
复制代码
一般步骤就是这样,基本就能完成AVFoundation的扫描二维码操作。
但是在运行过程中发现,AVCaptureSession
的startRunning
方法在调用的时候会稍微的卡顿。在Stack Overflow中看到的答案是
因为该方法会阻塞主线程,所以利用GCD的异步来执行相关的方法。我自己的代码如下:
dispatch_async(dispatchQueue, ^{
[self.captureSession startRunning];
dispatch_async(dispatch_get_main_queue(), ^{
[self.view.layer addSublayer:self.previewLayer];
self.captureMetadataOutput.rectOfInterest = [self.previewLayer metadataOutputRectOfInterestForRect:self.scanCrop];
self.maskView = [[ScannerMaskView alloc] initWithFrame:self.view.bounds cropFrame:self.scanCrop];
[self.view addSubview:self.maskView];
[self.view bringSubviewToFront:self.topBarView];
[self.indicatorView removeFromSuperview];
});
});
复制代码
开始running的同时,主线程开始更新相关的UI界面。目前还没发现其他问题。