原生实现扫描功能
前言:从iOS7开始,苹果就加入了相机二维码扫描的原生api
,继而iOS8之后也支持图片二维码识别功能。特此总结下实现的原理和过程。
1.权限
实现扫描功能需要用到相机和相册两个功能,所以在最先需要去项目的
info.plist
中设置用户访问权限
Privacy - Camera Usage Description - 访问相机用来拍照
Privacy - Photo Library Usage Description - 是否允许xxxAPP访问您的相册?
2.功能点分析
为了实现扫描的功能我们需要实现如下几个功能点:
- 进入页面打开相机
- 绘制相机的识别区域
- 对识别区以外的区域做区分图层处理
- 对摄像光感进行捕捉,判断是否开启闪光
- 对扫描物实现扫描识别功能
- 选取相册图片进行二维码识别
- 成功扫描后的回调
实现如下:
1.导入
#import <AVFoundationAVFoundation.h>
苹果原生框架,遵守AVCaptureMetadataOutputObjectsDelegate
协议,并且声明如下所需属性
/* 用来捕捉管理活动的对象 */
@property (strong,nonatomic)AVCaptureSession *session;
/* 设备 */
@property (strong,nonatomic)AVCaptureDevice *device;
/* 捕获输入 */
@property (strong,nonatomic)AVCaptureDeviceInput *input;
/* 捕获输出 */
@property (strong,nonatomic)AVCaptureMetadataOutput *output;
/* 背景 */
@property (strong,nonatomic)AVCaptureVideoPreviewLayer *ffView;
2.a. 在准初始化相机设备是,我们需要先进行设备,相机权限的判断,利用权限枚举
AVAuthorizationStatus
来捕获需要显式的用户还没有授予或拒绝的权限。
b. 推荐判断方式 (AVAuthorizationStatusRestricted || AVAuthorizationStatusDenied)具体枚举属性解释如下:
c. 在未开启权限的情况下推荐用户跳转到本APP的权限操作页面UIApplicationOpenSettingsURLString
供用户选择是否开启权限。
typedef NS_ENUM(NSInteger, AVAuthorizationStatus) {
AVAuthorizationStatusNotDetermined = 0,// 用户尚未做出选择这个应用程序的问候
AVAuthorizationStatusRestricted = 1,// 此应用程序没有被授权访问的照片数据。可能是家长控制权限
AVAuthorizationStatusDenied = 2,// 用户已经明确否认了这一照片数据的应用程序访问
AVAuthorizationStatusAuthorized = 3,// 用户已经授权应用访问照片数据
} NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;
3.核心代码实现 声明一个Block权限成功的回调方法,并调用
#pragma mark - 设备权限判断
- (void)setUpJudgmentWithScuessBlock:(dispatch_block_t)openSession
{
//权限
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied){
[DCScanTool setUpAlterViewWith:self WithReadContent:@"您未打开摄像权限。请在iPhone的“设置”-“隐私”-“相机”功能中,找到“申通APP”打开相机访问权限" WithLeftMsg:@"知道了" LeftBlock:nil RightMsg:@"前往" RightBliock:^{
NSURL *qxUrl = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
if([[UIApplication sharedApplication] canOpenURL:qxUrl]) { //跳转到本应用APP的权限界面
NSURL*url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
[[UIApplication sharedApplication] openURL:url];
}
}];
}else if(_device == nil){ //未找到设备
[DCScanTool setUpAlterViewWith:self WithReadContent:@"未检测到相机设备,请您先检查下设备是否支持扫描" WithLeftMsg:@"好的" LeftBlock:nil RightMsg:nil RightBliock:nil];
}else{ //识别到设备以及打开权限成功后回调
!openSession ? : openSession(); //回调
}
}
__weak typeof(self)weakSelf = self;
[weakSelf setUpJudgmentWithScuessBlock:^{
[self setUpPutInit]; //初始化
[self setUpFullFigureView]; //背景面
[self.session startRunning]; //开启扫描
}]; //设备权限判断成功回调
4.初始化,限制扫描区域,设置检测质量(质量越高扫描越精确),设置捕捉以及扫码类型。
- 在限制扫描区域上:可根据自己需求进行更改。这里有一个注意点
AVCaptureMetadataOutput
的一个属性rectOfInterest
利用这个属性就可以设置扫描区域了从而达到限定的作用。
/*!
@property rectOfInterest
@abstract
Specifies a rectangle of interest for limiting the search area for visual metadata.
@discussion
The value of this property is a CGRect that determines the receiver's rectangle of interest for each frame of video. The rectangle's origin is top left and is relative to the coordinate space of the device providing the metadata. Specifying a rectOfInterest may improve detection performance for certain types of metadata. The default value of this property is the value CGRectMake(0, 0, 1, 1). Metadata objects whose bounds do not intersect with the rectOfInterest will not be returned.
*/
@property(nonatomic) CGRect rectOfInterest NS_AVAILABLE_IOS(7_0);
解释:这个属性的值是一个CGRect,它决定了每个接收者的矩形。矩形的原点是左上角,相对于提供元数据的设备的坐标空间。指定一个rectOfInterest可以改进某些类型的元数据的检测性能。这个属性的默认值是cgrectdo(0,0,1,1)的值。元数据对象的边界与rectOfInterest不相交的对象不会被返回。
代码上理解:CGRectMake(扫描区域y/屏幕height, 扫描区域x/屏幕width, 扫描区域height/屏幕height, 扫描区域width/屏幕width)
//限制扫描区域
CGSize size = self.view.bounds.size;
CGRect cropRect = CGRectMake(DCScreenW * 0.1, DCScreenW * 0.3, DCScreenW * 0.8, DCScreenW * 0.8);
CGFloat p1 = size.height/size.width;
CGFloat p2 = 1920./1080.; //使用了1080p的图像输出
if (p1 < p2) {
CGFloat fixHeight = DCScreenW * 1920. / 1080.;
CGFloat fixPadding = (fixHeight - size.height)/2;
_output.rectOfInterest = CGRectMake((cropRect.origin.y + fixPadding)/fixHeight,cropRect.origin.x/size.width,cropRect.size.height/fixHeight,cropRect.size.width/size.width);
}else{
CGFloat fixWidth = self.view.frame.size.height * 1080. / 1920.;
CGFloat fixPadding = (fixWidth - size.width)/2;
_output.rectOfInterest = CGRectMake(cropRect.origin.y/size.height,(cropRect.origin.x + fixPadding)/fixWidth,cropRect.size.height/size.height,cropRect.size.width/fixWidth);
}
- 设置检测质量,质量越高扫描越精确,默认AVCaptureSessionPresetHigh
if ([_device supportsAVCaptureSessionPreset:AVCaptureSessionPreset1920x1080]) {
if ([_session canSetSessionPreset:AVCaptureSessionPreset1920x1080]) {
[_session setSessionPreset:AVCaptureSessionPreset1920x1080];
}
}else if ([_device supportsAVCaptureSessionPreset:AVCaptureSessionPreset1280x720]) {
if ([_session canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
[_session setSessionPreset:AVCaptureSessionPreset1280x720];
}
}
- 对扫描进行捕捉以及设置扫码类型
//捕捉
[_session setSessionPreset:AVCaptureSessionPresetHigh];
if ([_session canAddInput:self.input]){
[_session addInput:self.input];
}
if ([_session canAddOutput:self.output]){
[_session addOutput:self.output];
}
// 扫码类型
[self.output setMetadataObjectTypes:[NSArray arrayWithObjects:AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeQRCode, nil]];
- 遵守协议
<AVCaptureMetadataOutputObjectsDelegate>
走代理回调
_output = [AVCaptureMetadataOutput new];
[_output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()]; //设置输出流代理
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
if ([metadataObjects count] >0){
AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects objectAtIndex:0];
NSLog(@"扫描结果:%@",metadataObject.stringValue);
if (metadataObject.stringValue.length != 0) {
[self stopDeviceScanning]; //停止扫描
__weak typeof(self)weakSelf = self;
[DCScanTool setUpAlterViewWith:self WithReadContent:[NSString stringWithFormat:@"扫描结果为:%@",metadataObject.stringValue] WithLeftMsg:@"好的" LeftBlock:^{
[weakSelf.navigationController popViewControllerAnimated:YES];
} RightMsg:nil RightBliock:nil];
}
}
}
3.界面图层处理
可根据具体UI图去设置,这样仅介绍几个layer的方法和展示下demo实现样式
- 给屏幕layer绘上灰色背景
#pragma mark - 灰色背景
- (void)drawWhiteRect:(CGContextRef)ctx rect:(CGRect)rect {
CGContextStrokeRect(ctx, rect);
CGContextSetRGBStrokeColor(ctx, 1, 1, 1, 1);
CGContextSetLineWidth(ctx, 1);
CGContextAddRect(ctx, rect);
CGContextStrokePath(ctx);
}
- 在画好灰色背景的情况下,挖去识别矩形框
#pragma mark - 挖去识别矩形框
- (void)drawCenterClearRect :(CGContextRef)ctx rect:(CGRect)rect {
CGContextClearRect(ctx, rect);
}
- 调用layer画图层的方法在
- (void)drawRect:(CGRect)rect
方法中进行调用
4.光感识别
- 介绍一下光感识别的属性
AVCaptureVideoDataOutput
在苹果官方文档中以及AVFoundation
框架中的介绍如下
A capture output that records video and provides access to video frames for processing.
解释:一个记录视频的捕获输出,并可以对视频帧进行处理访问。
/*!
@class AVCaptureVideoDataOutput
@abstract
AVCaptureVideoDataOutput is a concrete subclass of AVCaptureOutput that can be used to process uncompressed or compressed frames from the video being captured.
@discussion
Instances of AVCaptureVideoDataOutput produce video frames suitable for processing using other media APIs. Applications can access the frames with the captureOutput:didOutputSampleBuffer:fromConnection: delegate method.
*/
解释大致如下:可用于从捕获的视频中处理未压缩或压缩的帧。具体可访问`captureOutput:didOutputSampleBuffer:fromConnection:`委托方法。
设置一个
FlashButton
利用其Selected
和Normal
属性用来开启或关闭闪光灯
#pragma mark - 闪关灯按钮点击回调
- (void)flashButtonClick:(UIButton *)button
{
if (button.selected == NO) {
[DCScanTool openFlashlight];
} else {
[DCScanTool closeFlashlight];
}
button.selected = !button.selected;
}
#pragma mark - 打开手电筒
+ (void)openFlashlight {
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error = nil;
if ([captureDevice hasTorch]) {
BOOL locked = [captureDevice lockForConfiguration:&error];
if (locked) {
captureDevice.torchMode = AVCaptureTorchModeOn;
[captureDevice unlockForConfiguration];
}
}
}
#pragma mark - 关闭手电筒
+ (void)closeFlashlight {
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if ([device hasTorch]) {
[device lockForConfiguration:nil];
[device setTorchMode: AVCaptureTorchModeOff];
[device unlockForConfiguration];
}
}
声明
AVCaptureVideoDataOutput
输出流,设置其代理,将其AVCaptureSession
对象中,在代理方法中获取设备摄像光感。代码实现如下:
/* 输出流 */
@property (nonatomic, strong) AVCaptureVideoDataOutput *videoDataOutput;
//设备输出流
self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
[_videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
[_session addOutput:_videoDataOutput]; //添加到sesson,识别光线强弱
#pragma mark - <AVCaptureVideoDataOutputSampleBufferDelegate>
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
// 调用这个方法的时候内存稳定
CFDictionaryRef metadataDict = CMCopyDictionaryOfAttachments(NULL,sampleBuffer, kCMAttachmentMode_ShouldPropagate);
NSDictionary *metadata = [[NSMutableDictionary alloc] initWithDictionary:(__bridge NSDictionary*)metadataDict];
CFRelease(metadataDict);
NSDictionary *exifMetadata = [[metadata objectForKey:(NSString *)kCGImagePropertyExifDictionary] mutableCopy];
float brightnessValue = [[exifMetadata objectForKey:(NSString *)kCGImagePropertyExifBrightnessValue] floatValue]; //光线强弱度
if (!self.flashButton.selected) {
self.flashButton.alpha = (brightnessValue < 1.0) ? 1 : 0;
}
}
5.获取相册图片扫描
在获取相册图片的同时处理要求用户同意info中的协议,还需要在页面遵循两个代理UIImagePickerControllerDelegate``UINavigationControllerDelegate
,在其代理方法的回调中进行扫描识别。这里有一个小注意点,很多人在调用相册方法的时候出现的返回等提示是英文的,可自行在Info.plist
进行更改设置成中文如下
Info.plist
首先在点击用户相册跳转时利用
isSourceTypeAvailable
方法先进行判断是否已经同意协议,成功后设置代理,在回调中对选中的图片进行识别。
这里主要利用CIDetector
来识别。
苹果官方解释如下:An image processor that identifies notable features (such as faces and barcodes) in a still image or video.
解释 :一种图像处理器,它能在静态图像或视频中识别出显著特征(如人脸和条形码)。
- 核心代码
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary]) {
UIAlertController *alter = [UIAlertController alertControllerWithTitle:@"温馨提示" message:@"无法访问相册" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){
}];
[alter addAction:okAction];
[self presentViewController:alter animated:YES completion:nil];
return;
}
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
picker.view.backgroundColor = [UIColor whiteColor];
picker.delegate = self;
[self showDetailViewController:picker sender:nil];
#pragma mark - UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
UIImage *image = [DCScanTool resizeImage:info[UIImagePickerControllerOriginalImage] WithMaxSize:CGSizeMake(1000, 1000)];
dispatch_async(dispatch_get_global_queue(0, 0), ^{ //异步
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{CIDetectorAccuracy : CIDetectorAccuracyHigh}];
CIImage *selImage = [[CIImage alloc] initWithImage:image];
NSArray *features = [detector featuresInImage:selImage];
NSMutableArray *arrayM = [NSMutableArray arrayWithCapacity:features.count];
for (CIQRCodeFeature *feature in features) {
[arrayM addObject:feature.messageString];
}
__weak typeof(self)weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"扫描结果%@",arrayM.copy);
if (arrayM.copy != nil && ![arrayM isKindOfClass:[NSNull class]] && arrayM.count != 0) {
[weakSelf dismissViewControllerAnimated:YES completion:nil];
[DCScanTool setUpAlterViewWith:self WithReadContent:[NSString stringWithFormat:@"扫描结果为:%@",arrayM.copy] WithLeftMsg:@"好的" LeftBlock:^{
[weakSelf stopDeviceScanning]; //停止扫描
[weakSelf.navigationController popViewControllerAnimated:YES];
} RightMsg:nil RightBliock:nil];
}else{
[weakSelf dismissViewControllerAnimated:YES completion:nil];
[DCScanTool setUpAlterViewWith:self WithReadContent:@"未能识别到任何二维码,请重新识别" WithLeftMsg:@"好的" LeftBlock:nil RightMsg:nil RightBliock:nil];
}
});
});
}