iOS-AVCaptureSession扫描读取二维码

4,525 阅读7分钟

二维码简单介绍

二维码(Quick Response Code,QRCode)是一种由水平和垂直两个方向上的线条设计而成的二维条形码,可以存储数据信息,本文主要是介绍二维码的读取(不涉及二维码的生成)

二维码读取

介绍

读取二维码就是通过扫描二维码图像来获取其中的数据信息,任何条形码的扫描都是基于视频采集,因此需要用到AVFoundation框架

以下是AVFoundation库的概述

The AVFoundation framework combines six major technology areas that together encompass a wide range of tasks for capturing, processing, synthesizing, controlling, importing and exporting audiovisual media on Apple platforms.

AVFoundation框架结合了六个主要技术领域,这些领域共同涵盖了在Apple平台上捕获,处理,合成,控制,导入和导出视听媒体的广泛任务。

对于二维码的读取,我们主要用到该库中的Capture部分,即AVCaptureSession类,以下是其概述

image.png 可以看到该类继承自NSObject,主要功能是用于管理capture(捕获)活动并协调从输入设备到捕获设备的数据流

扫描过程概述

扫描二维码的过程即从摄像头捕获二维码图像(input)到解析出字符串内容(output)的过程,该过程主要就是通过AVCaptureSession对象来实现

AVCaptureSession对象用于协调从输入到输出的数据流,在执行过程中,需要先将输入和输出添加到该对象中,然后通过发送startRunningstopRunning消息来启动或停止数据流,最后通过AVCaptureVideoPreviewLayer对象来将捕获的视频显示在屏幕上

其中,输入对象通常是AVCaptureDeviceInput对象,通过AVCaptureDevice的实例来获得,输出对象通常是AVCaptureMetaDataOutput对象,该对象是读取二维码的核心部分,需要结合AVCaptureMetaDataOutputObjectsDelegate协议结合使用,可以捕获在输入设备中找到的任何元数据(metadata就是元数据的意思),并将其转换为字符串的格式

接下来我们来结合代码详细说明每个过程

具体步骤

1. 导入AVFoundation框架

#import <AVFoundation/AVFoundation.h>

2. 判断权限

由于扫描二维码过程需要用到摄像头,因此我们需要设置摄像头的权限并进行判断

第一步:设置权限

有两种方式设置权限

  1. 直接在info.plist文件中添加

image.png 再通过source code的方式打开,可以看到自动添加了两行代码

image.png

因此我们其实也可以直接在源代码中添加对应的代码,就是下面的方法

  1. 通过source code的方式在info.plist文件中添加
<key>NSCameraUsageDescription</key>
<string>获取相机权限</string>

image.png

举一反三,对于其他的需要获取权限设置也可以通过如上两个方式实现,例如麦克风、地理位置等,如图

image.png

第二步:判断权限

代码简写了,核心的部分如下

#pragma mark --判断权限
-(void)judgeAuthority{
//判断权限的方法
    [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
        //要放到主线程中刷新
        dispatch_async(dispatch_get_main_queue(), ^{
            // 若已授权
            if (granted) {
                 //调用扫描二维码的方法
            } else {
                //若未授权,提示弹窗
                ....
            }
        });
    }];
}

且其中最核心的部分就是requestAccessForMediaType:(AVMediaType)mediaType completionHandler:(void (^)(BOOL granted))handler ; 这个方法,用于请求权限,包含两个参数

  • 第一个参数AVMediaType是媒体类型 有如下几种类型

image.png

  • 第二个参数是一个block块,写相关判断的代码即可 关于提示弹窗,我们用的是UIAlertController
  1. 创建UIAlertController对象
NSString *title = @"请在iPhone的“设置-隐私-相机“选项中,允许App访问你的相机";
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:title preferredStyle:UIAlertControllerStyleAlert];
  1. 创建UIAlertAction对象,即按钮
UIAlertAction *conform = [UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
                    NSLog(@"点击了确认按钮");
                }];
                UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
                    NSLog(@"点击了取消按钮");
                }];
  1. 将按钮添加到alert中
[alert addAction:conform];
[alert addAction:cancel];
  1. 显示弹窗
[self presentViewController:alert animated:YES completion:nil];

显示效果如下,首先会系统自动弹窗请求相机权限

IMG_0006.PNG

若点击了不允许,即无法拿到相机权限,显示弹窗

IMG_0007.PNG

3. 创建AVCaptureSession对象

@property (nonatomic, strong) AVCaptureSession *captureSession;

_captureSession = [[AVCaptureSession alloc]init];

4. 为AVCaptureSession对象添加输入输出

//1. 初始化设备
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

//2. 创建输入,基于device实例的输入
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];

//3. 创建输出
AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init];

//4. 添加输入输出
[_captureSession addInput:deviceInput];
[_captureSession addOutput:metadataOutput];

5. 配置AVCaptureMetaDataOutput对象

首先是设置代理,然后是设置元数据类型

//1. 设置代理
[metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

//2. 设置元数据类型,因为这里是二维码的扫描,所以数据类型是AVMetadataObjectTypeQRCode,注意是需要传入数组
[metadataOutput setMetadataObjectTypes:@[AVMetadataObjectTypeQRCode]];

6. 创建并设置AVCaptureVideoPreviewLayer对象来显示捕获到的视频

@property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewLayer;//展示layer

//1. 实例化预览涂层图层
_videoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];

//2. 设置预览图层填充方式
[_videoPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];

//3. 设置图层的frame
[_videoPreviewLayer setFrame:_viewPreview.layer.bounds];

//4. 将图层添加到预览view的图层上
[_viewPreview.layer addSublayer:_videoPreviewLayer];

//5. 设置扫描范围,这里使用的是相对位置
metadataOutput.rectOfInterest = CGRectMake(0.2f, 0.2f, 0.8f, 0.8f);

这里用到了rectOfInterest这个属性,是用于设置元数据的搜索区域的,确定矩形,矩形的坐标原点位于左上角

7. 实现代理方法

#pragma mark --AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    //判断是否正在读取数据
    if (!_isReading) {
        //没有读取,返回
        return;
    }
    //若metadataObjects.count > 0,代表扫描到二维码
    if (metadataObjects.count > 0) {
        _isReading = NO;
        //
        AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects[0];
        NSString *result = metadataObject.stringValue;
        if (self.resultBlock) {
            self.resultBlock(result);
        }
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self.navigationController popViewControllerAnimated:YES];
    }
}

开发过程中遇到的问题和解决方法

其实上面也有一些已经说了例如权限设置这些,以下还有几个点

1. 程序运行黑屏

AppDelegate.h中添加UIWindow属性

@property (nonatomic, strong)UIWindow *window;

2. 导航栏不显示title

title属性是从UIViewController上面继承过来的,而不是UINavigationController上面的名字

由于UINavigationController属于容器,所以最少需要一个RootVIewController

然后在RootViewController的viewDidLoad设置title而不是在UINavigationController的subclass中设置

self.navigationController.title = @"扫一扫";  //原本的代码
self.title = @"扫一扫"; //修改为self即可

3. 按钮不居中

//原本的代码,发现按钮水平方向偏右,竖直方向”居中“
btn.frame = CGRectMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0, 80, 40);
//解决方法,水平方向 - 40 即可(还没有纠结原因)
btn.frame = CGRectMake(self.view.bounds.size.width / 2.0 - 40 , self.view.bounds.size.height / 2.0, 80, 40);

但其实有更好的方法就是直接设置center 即可

//先确定frame,定长度和宽度
btn.frame = CGRectMake(0,0, 80, 40);
//确定中心,即坐标
btn.center = self.view.center;

4. block属性传值

简单梳理block属性传值的大致代码

//ViewController.m

//点击按钮时调用jumpToScanVC方法
[btn addTarget:self action:@selector(jumpToScanVC) forControlEvents:UIControlEventTouchUpInside];

-(void)jumpToScanVC{
    SecondViewController *secondVC = [[SecondViewController alloc]init];
    secondVC.secondBlock = ^(NSString * _Nonnull string) {
        self.label.text = string;
    };
    [self.navigationController pushViewController:secondVC animated:NO];
}
// SecondViewController.h

//定义block属性
@interface SecondViewController : UIViewController
@property (nonatomic, copy) void(^secondBlock)(NSString *string);
@end
// SecondViewController.m

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    if (_secondBlock) {
        _secondBlock(@"hahaha");
    }
    [self.navigationController popViewControllerAnimated:NO];
}

可以看到在第二个控制器中调用了block并将值传递到了第一个控制器

5. UIAlertController的用法

扫描结果显示并弹窗,但是控制器没有被成功pop出去,报错显示

popViewControllerAnimated: called on <UINavigationController 0x101827e00> while an existing transition or presentation is occurring; the navigation stack will not be updated.

其实就是我们UIAlertController的弹窗动画和pop控制器的动画冲突了

但其实我们的navigation stack已经pop掉了,只是无法显示动画

因此我们可以通过延迟执行来达到先完成弹窗动画,再完成pop动画

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self.navigationController popViewControllerAnimated:YES];
        });

6. 点击扫描后跳转了页面但是没有显示扫描框,控制器卡死

原因是没有放到主线程中去刷新UI导致了卡死

image.png

7. 防止NSTimer导致无法释放的问题

因为设置了扫描线(其实就是一个装饰,显得专业一点),扫描线的移动用到了计时器,但是注意计时器的使用很可能导致内存无法释放的情况,所以我们要事先将NSTimer对象置空

#pragma mark --结束
-(void)stopRunning{
    //判断定时器是否正在工作,若还在工作另起暂停并置空
    if ([_timer isValid]) {
        //正在工作就使其失效
        [_timer invalidate];
        //并给定时器赋值nil
        _timer = nil;
    }
    [self.captureSession stopRunning];
    ...
}

运行界面演示

二维码图片

%E4%BA%8C%E7%BB%B4%E7%A0%81%E5%9B%BE%E7%89%87_5%E6%9C%8817%E6%97%A514%E6%97%B657%E5%88%8612%E7%A7%92.png

IMG_0008.PNG

IMG_0009.PNG

IMG_0010.PNG


参考博客

ios使用AVFoundation读取二维码的方法

iOS利用AVCaptureSession实现二维码扫描