项目中要添加扫码功能。虽然网上已经有很多Demo,但还是想自己好好看看系统文档,亲自踩下坑。将学习过程记录下。
Demo已经放到GitHub上了。主要有以下功能特点
- 支持iOS8.0以上系统;
- 采用系统API扫描识别二维码;
- 采用系统API识别相册中的二维码;若识别失败,调用ZBar方法再识别一次;
- 采用系统API生成二维码,二维码中间可以添加一个小图片,可生成彩色二维码;
- 扫码界面布局仿照微信。
- 可打开手电筒、切换前后置相机
效果图
概述
采用AVFoundataion
框架来进行视频采集。至少需要以下几步
- 一个
AVCaptureDevice
的实例,用来描述输入设备,比如相机或者麦克风 - 一个
AVCaptureInput
子类的实例,用来从输入设备配置端口。此处使用的是子类AVCaptureDeviceInput
- 一个
AVCaptureOutput
子类的实例,用来管理输出数据。此处使用的是子类AVCaptureMetadataOutput
- 一个
AVCaptureSession
的实例,用来协调输入输出流
如果要向用户展示正在录制的内容,可以使用AVCaptureVideoPreviewLayer
(CALayer的子类)的实例
Capture Session
AVCaptureSession
这个类是管理采集数据的中枢。我们可以使用这个类去管理从输入到输出的数据流。添加想要的输入输出数据到这个session,然后调用startRunning
开始采集数据,调用stopRunning
停止数据流。
AVCaptureSession *session = [[AVCaptureSession alloc] init];
// Add inputs and outputs.
[session startRunning];
startRunning
会同步执行并阻塞当前线程,直到AVCaptureSession
启动或者启动失败。如果启动失败,我们会接收到一个通知AVCaptureSessionRuntimeErrorNotification
。
注意:由于startRunning
执行需要花费一段时间,并且会阻塞当前线程,因此我们应该在一个串行子线程中去执行这个方法,这样可以避免主线程UI被锁。
参考例子AVCam-iOS: Using AVFoundation to Capture Images and Movies
配置Session
设置图片的质量和分辨率。系统提供以下几种配置。当需要识别的图片比较小时,就选高分辨率,反之则选择低分辨率。一般来说选择AVCaptureSessionPreset1280x720
或者AVCaptureSessionPreset640x480
就可以了。
在设置之前,先要判断系统是否支持这个分辨率
if ([session canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
session.sessionPreset = AVCaptureSessionPreset1280x720;
}
else {
// Handle the failure.
}
如果需要根据实际情况选择不同的分辨率,可以使用 beginConfiguration
, commitConfiguration
这两个方法
[session beginConfiguration];
// Remove an existing capture device.
// Add a new capture device.
// Reset the preset.
[session commitConfiguration];
监听CaptureSession状态
session开始或者停止运行,发生中断等,都会发出一个通知。
系统提供了如下几种通知,可以监听并且做一些异常判断。
AVCaptureSessionRuntimeErrorNotification
AVCaptureSessionDidStartRunningNotification
AVCaptureSessionDidStopRunningNotification
AVCaptureSessionWasInterruptedNotification
AVCaptureSessionInterruptionEndedNotification
添加输入设备
NSError *error;
AVCaptureDeviceInput *captureDeviceInput =
[AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
if (!captureDeviceInput) {
// Handle the error appropriately.
}
AVCaptureSession *captureSession = <#Get a capture session#>;
if ([captureSession canAddInput:captureDeviceInput]) {
[captureSession addInput:captureDeviceInput];
}
else {
// Handle the failure.
}
获取输出数据
AVCaptureSession *captureSession = <#Get a capture session#>;
AVCaptureMetadataOutput *outPut = [[AVCaptureMetadataOutput alloc] init];
if ([captureSession canAddOutput:outPut]) {
[captureSession addOutput:outPut];
//设置输出执行的委托以及委托运行的线程
[outPut setMetadataObjectsDelegate:self queue:_metadataObjectsQueue];
//设置支持的类型,此处设置为支持所有的类型
outPut.metadataObjectTypes = [self.videoDeviceOutput availableMetadataObjectTypes];
}
else {
// Handle the failure.
}
输出代理方法
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
for (AVMetadataObject *object in metadataObjects) {
AVMetadataMachineReadableCodeObject *codeObj = (AVMetadataMachineReadableCodeObject *)object;
NSString *QRMessage = codeObj.stringValue;
}
}
如果能够正确识别二维码,那么QRMessage就是识别出的数据
可参考例子AVCamBarcode: Using AVFoundation to Detect Barcodes and Faces
通过api的说明可知,当采集到数据后,可在此方法中获得输出数据。此委托方法会运行在上面设置代理时指定的线程中。由于这个方法会很频繁的调用,因此我们必须注意性能问题。
至此,调用相机扫描二维码的功能就完成了。下面看一下如何识别相册中的二维码。
识别图片中的二维码
使用了CIDetector
类来识别图片中的二维码。除了二维码,它还可以识别人脸等其他类型。系统提供了几种类型:
CIDetectorTypeFace
CIDetectorTypeRectangle
CIDetectorTypeQRCode
识别的代码如下
- (NSString *)decodeQRImage:(UIImage*)image
{
CIContext *context = [CIContext contextWithOptions:nil];
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
CIImage *ciImage = [CIImage imageWithCGImage:image.CGImage];
NSArray *features = [detector featuresInImage:ciImage];
//如果使用系统发发不能识别图中二维码,再采用ZBar尝试一次
if (features.count > 0) {
CIQRCodeFeature *feature = [features firstObject];
return feature.messageString;
}else {
//采用ZBar识别
CGImageRef cgImageRef = image.CGImage;
ZBarQRDecoder *QRDecoder = [[ZBarQRDecoder alloc] init];
NSArray *symbols = [QRDecoder scanImage:cgImageRef];
for (ZBarSymbol *symbol in symbols) {
return symbol.data;
}
}
return nil;
}
注意 NSArray *features = [detector featuresInImage:ciImage];
这个方法不稳定,在某些设备上会存在识别失败的问题。看网上有人说在iPhone5、iPhone4等老设备上会存在识别不出来的问题。我自己亲测的是,使用5c拍摄的二维码全都识别不了。这个应该是苹果API的问题。为了解决这个问题,引用了ZBar,当使用系统API识别失败时,用ZBar再尝试一次。
ZBarSDK
ZBarSDK
在github上看到ZBar最近的更新也是4年前了。虽然已经这么久不维护了,但是既然当年这份代码能运行的很好,现在二维码的标准也没有什么更新,那这份代码依然能够很好的运行。完全可以满足我目前的需求。
从GitHub上下载的ZBar工程有很多问题,运行起来也会有很多错误。因为我的项目中只需要使用识别图片的功能,因此我将这个库整理了一下,删掉了不必要的操作。具体也是看我的Demo。
需要注意的两点是
- 需要在工程中添加
libiconv.2.4.0.tbd
链接库 -
ZBarSymbol.m
,ZBarImageScanner.m
,ZBarImage.m
三个文件要禁止使用ARC,Build Phases中添加-fno-objc-arc
- 在工程中导入头文件
ZBarSDK.h
,调用ZBarQRDecoder
类的scanImage
方法来扫描图片。
生成二维码
使用CIFilter类来创建自定义的二维码滤镜,生成二维码图片。
关于自定义滤镜的使用,可以参考官方文档
使用CIQRCodeGenerator
创建一个二维码滤镜
- (void)generateQRCode
{
// Create a new filter with the given name.
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
if (!filter) return;
// -setDefaults instructs the filter to configure its parameters
// with their specified default values.
[filter setDefaults];
//二维码数据
NSString *QRMessage = @"http://www.jianshu.com/users/3688d37243de/latest_articles";
// 官方建议使用 NSISOLatin1StringEncoding 来编码,但经测试这种编码对中文或表情无法生成,改用 NSUTF8StringEncoding 就可以了。
NSData *inputData = [QRMessage dataUsingEncoding:NSUTF8StringEncoding];
// 设置过滤器的输入值, KVC赋值
[filter setValue:inputData forKey:@"inputMessage"];
CIImage *outputImage = [filter outputImage];
// 输出的图片比较小,需要放大。此处放大20倍
outputImage = [outputImage imageByApplyingTransform:CGAffineTransformMakeScale(20, 20)];
UIImage *QRImage = [UIImage imageWithCIImage:outputImage];
_QRCodeImageView.image = QRImage;
}
为二维码添加中间的小图片
此处直接在二维码的中心画一个UIImageView
-(void)drawSmallIcon
{
UIImageView *iconView = [[UIImageView alloc]init];
iconView.layer.cornerRadius = 4.0f;
iconView.layer.borderColor = [UIColor whiteColor].CGColor;
iconView.layer.borderWidth = 2.0f;
[_QRCodeImageView addSubview:iconView];
UIImage *avator = [UIImage imageNamed:@"avator.jpg"];
avator = [avator resizeToSize:CGSizeMake(100, 100)];
iconView.image = avator;
//设置小图片为二维码的20%
[iconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(0);
make.width.equalTo(_QRCodeImageView.mas_width).multipliedBy(0.2f);
make.height.equalTo(iconView.mas_width);
}];
}
除了文中指出的文档和demo外,还参考了以下文章