二维码(Quick Response Code,QRCode)是一种由水平和垂直两个方向上的线条设计而成的二维条形码,能够存储数据信息,本文主要是介绍二维码的读取(不涉及二维码的生成)ios
读取二维码就是经过扫描二维码图像来获取其中的数据信息,任何条形码的扫描都是基于视频采集,所以须要用到AVFoundation框架objective-c
如下是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.markdown
AVFoundation框架结合了六个主要技术领域,这些领域共同涵盖了在Apple平台上捕获,处理,合成,控制,导入和导出视听媒体的普遍任务。框架
对于二维码的读取,咱们主要用到该库中的Capture部分,即AVCaptureSession类,如下是其概述async
能够看到该类继承自NSObject,主要功能是用于管理capture(捕获)活动并协调从输入设备到捕获设备的数据流ide
扫描二维码的过程即从摄像头捕获二维码图像(input)到解析出字符串内容(output)的过程,该过程主要就是经过AVCaptureSession对象来实现oop
AVCaptureSession
对象用于协调从输入到输出的数据流,在执行过程当中,须要先将输入和输出添加到该对象中,而后经过发送startRunning
和stopRunning
消息来启动或中止数据流,最后经过AVCaptureVideoPreviewLayer
对象来将捕获的视频显示在屏幕上动画
其中,输入对象一般是AVCaptureDeviceInput
对象,经过AVCaptureDevice
的实例来得到,输出对象一般是AVCaptureMetaDataOutput
对象,该对象是读取二维码的核心部分,须要结合AVCaptureMetaDataOutputObjectsDelegate
协议结合使用,能够捕获在输入设备中找到的任何元数据(metadata就是元数据的意思),并将其转换为字符串的格式ui
接下来咱们来结合代码详细说明每一个过程
#import <AVFoundation/AVFoundation.h>
复制代码
因为扫描二维码过程须要用到摄像头,所以咱们须要设置摄像头的权限并进行判断
第一步:设置权限
有两种方式设置权限
再经过source code的方式打开,能够看到自动添加了两行代码
所以咱们其实也能够直接在源代码中添加对应的代码,就是下面的方法
<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
是媒体类型有以下几种类型
关于提示弹窗,咱们用的是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 addAction:conform];
[alert addAction:cancel];
复制代码
[self presentViewController:alert animated:YES completion:nil];
复制代码
显示效果以下,首先会系统自动弹窗请求相机权限
若点击了不容许,即没法拿到相机权限,显示弹窗
@property (nonatomic, strong) AVCaptureSession *captureSession;
_captureSession = [[AVCaptureSession alloc]init];
复制代码
//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];
复制代码
首先是设置代理,而后是设置元数据类型
//1. 设置代理
[metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
//2. 设置元数据类型,由于这里是二维码的扫描,因此数据类型是AVMetadataObjectTypeQRCode,注意是须要传入数组
[metadataOutput setMetadataObjectTypes:@[AVMetadataObjectTypeQRCode]];
复制代码
@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
这个属性,是用于设置元数据的搜索区域的,肯定矩形,矩形的坐标原点位于左上角
#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];
}
}
复制代码
其实上面也有一些已经说了例如权限设置这些,如下还有几个点
在AppDelegate.h
中添加UIWindow
属性
@property (nonatomic, strong)UIWindow *window;
复制代码
title属性是从UIViewController上面继承过来的,而不是UINavigationController上面的名字
因为UINavigationController属于容器,因此最少须要一个RootVIewController
而后在RootViewController的viewDidLoad设置title而不是在UINavigationController的subclass中设置
self.navigationController.title = @"扫一扫"; //本来的代码
self.title = @"扫一扫"; //修改成self便可
复制代码
//本来的代码,发现按钮水平方向偏右,竖直方向”居中“
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;
复制代码
简单梳理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并将值传递到了第一个控制器
扫描结果显示并弹窗,可是控制器没有被成功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];
});
复制代码
缘由是没有放到主线程中去刷新UI致使了卡死
由于设置了扫描线(其实就是一个装饰,显得专业一点),扫描线的移动用到了计时器,可是注意计时器的使用极可能致使内存没法释放的状况,因此咱们要事先将NSTimer对象置空
#pragma mark --结束
-(void)stopRunning{
//判判定时器是否正在工做,若还在工做另起暂停并置空
if ([_timer isValid]) {
//正在工做就使其失效
[_timer invalidate];
//并给定时器赋值nil
_timer = nil;
}
[self.captureSession stopRunning];
...
}
复制代码
二维码图片
参考博客