二维码简单介绍
二维码(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类,以下是其概述
可以看到该类继承自NSObject,主要功能是 用于管理capture(捕获)活动并协调从输入设备到捕获设备的数据流
扫描过程概述
扫描二维码的过程即从摄像头捕获二维码图像(input)到解析出字符串内容(output)的过程,该过程主要就是通过AVCaptureSession对象来实现
AVCaptureSession
对象用于协调从输入到输出的数据流
,在执行过程中,需要先将输入和输出添加到该对象中,然后通过发送
startRunning
和
stopRunning
消息来启动或停止数据流,最后通过
AVCaptureVideoPreviewLayer
对象来将捕获的视频显示在屏幕上
其中,输入对象通常是
AVCaptureDeviceInput
对象,通过
AVCaptureDevice
的实例来获得,输出对象通常是
AVCaptureMetaDataOutput
对象,该对象是读取二维码的核心部分,需要结合
AVCaptureMetaDataOutputObjectsDelegate
协议结合使用,可以捕获在输入设备中找到的任何元数据(metadata就是元数据的意思),并将其转换为字符串的格式
接下来我们来结合代码详细说明每个过程
1. 导入AVFoundation框架
#import <AVFoundation/AVFoundation.h>
2. 判断权限
由于扫描二维码过程需要用到摄像头,因此我们需要设置摄像头的权限并进行判断
第一步:设置权限
有两种方式设置权限
直接在info.plist文件中添加
再通过source code的方式打开,可以看到自动添加了两行代码
因此我们其实也可以直接在源代码中添加对应的代码,就是下面的方法
通过source code的方式在info.plist文件中添加
<key>NSCameraUsageDescription</key>
<string>获取相机权限</string>
举一反三,对于其他的需要获取权限设置也可以通过如上两个方式实现,例如麦克风、地理位置等,如图
第二步:判断权限
代码简写了,核心的部分如下
#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
是媒体类型
有如下几种类型
第二个参数是一个block块,写相关判断的代码即可
关于提示弹窗,我们用的是UIAlertController
创建UIAlertController
对象
NSString *title = @"请在iPhone的“设置-隐私-相机“选项中,允许App访问你的相机";
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:title preferredStyle:UIAlertControllerStyleAlert];
创建UIAlertAction
对象,即按钮
UIAlertAction *conform = [UIAlertAction actionWithTitle:@"确认" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"点击了确认按钮");
UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"点击了取消按钮");
将按钮添加到alert中
[alert addAction:conform];
[alert addAction:cancel];
[self presentViewController:alert animated:YES completion:nil];
显示效果如下,首先会系统自动弹窗请求相机权限
若点击了不允许,即无法拿到相机权限,显示弹窗
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);
// 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导致了卡死
7. 防止NSTimer导致无法释放的问题
因为设置了扫描线(其实就是一个装饰,显得专业一点),扫描线的移动用到了计时器,但是注意计时器的使用很可能导致内存无法释放的情况,所以我们要事先将NSTimer对象置空
#pragma mark --结束
-(void)stopRunning{
//判断定时器是否正在工作,若还在工作另起暂停并置空
if ([_timer isValid]) {
//正在工作就使其失效
[_timer invalidate];
//并给定时器赋值nil
_timer = nil;
[self.captureSession stopRunning];
运行界面演示
二维码图片
运行界面演示