二维码(quick response code,简称qr code)是由水平和垂直两个方向上的线条设计而成的一种二维条形码(barcode)。可以编码网址、电话号码、文本等内容,能够存储大量的数据信息。自ios 7以来,二维码的生成和读取只需要使用core image框架和avfoundation框架就能轻松实现。在这里,我们主要介绍二维码的读取。关于二维码的生成,可以查看使用cifilter生成二维码文章中的介绍。
1 二维码的读取
读取二维码也就是通过扫描二维码图像以获取其所包含的数据信息。需要知道的是,任何条形码(包括二维码)的扫描都是基于视频采集(video capture),因此需要使用avfoundation框架。
扫描二维码的过程即从摄像头捕获二维码图像(input)到解析出字符串内容(output)的过程,主要是通过avcapturesession对象来实现的。该对象用于协调从输入到输出的数据流,在执行过程中,需要先将输入和输出添加到avcapturesession对象中,然后通过发送startrunning或stoprunning消息来启动或停止数据流,最后通过avcapturevideopreviewlayer对象将捕获的视频显示在屏幕上。在这里,输入对象通常是avcapturedeviceinput对象,主要是通过avcapturedevice的实例来获得,而输出对象通常是avcapturemetadataoutput对象,它是读取二维码的核心部分,与avcapturemetadataoutputobjectsdelegate协议结合使用,可以捕获在输入设备中找到的任何元数据,并将其转换为可读的格式。下面是具体步骤:
1、导入avfoundation框架。
1
|
#import <avfoundation/avfoundation.h>
|
2、创建一个avcapturesession对象。
1
|
avcapturesession *capturesession = [[avcapturesession alloc] init];
|
3、为avcapturesession对象添加输入和输出。
1
2
3
4
5
6
7
8
9
10
|
// add input
nserror *error;
avcapturedevice *device = [avcapturedevice defaultdevicewithmediatype:avmediatypevideo];
avcapturedeviceinput *deviceinput = [avcapturedeviceinput deviceinputwithdevice:device error:&error];
[capturesession addinput:deviceinput];
// add output
avcapturemetadataoutput *metadataoutput = [[avcapturemetadataoutput alloc] init];
[capturesession addoutput:metadataoutput];
|
4、配置avcapturemetadataoutput对象,主要是设置代理和要处理的元数据对象类型。
1
2
3
|
dispatch_queue_t queue = dispatch_queue_create( "myqueue" , null);
[metadataoutput setmetadataobjectsdelegate:self queue:queue];
[metadataoutput setmetadataobjecttypes:@[avmetadataobjecttypeqrcode]];
|
需要注意的是,一定要在输出对象被添加到capturesession之后才能设置要处理的元数据类型,否则会出现下面的错误:
terminating app due to uncaught exception 'nsinvalidargumentexception', reason: [avcapturemetadataoutput setmetadataobjecttypes:] unsupported type found - use -availablemetadataobjecttypes'
5、创建并设置avcapturevideopreviewlayer对象来显示捕获到的视频。
1
2
3
4
|
avcapturevideopreviewlayer *previewlayer = [[avcapturevideopreviewlayer alloc] initwithsession:capturesession];
[previewlayer setvideogravity:avlayervideogravityresizeaspectfill];
[previewlayer setframe:self.view.bounds];
[self.view.layer addsublayer:previewlayer];
|
6、给avcapturesession对象发送startrunning消息以启动视频捕获。
1
|
[capturesession startrunning];
|
7、实现avcapturemetadataoutputobjectsdelegate的captureoutput:didoutputmetadataobjects:fromconnection:方法来处理捕获到的元数据,并将其读取出来。
1
2
3
4
5
6
7
8
9
10
|
- ( void )captureoutput:(avcaptureoutput *)output didoutputmetadataobjects:(nsarray<__kindof avmetadataobject *> *)metadataobjects fromconnection:(avcaptureconnection *)connection
{
if (metadataobjects != nil && metadataobjects.count > 0) {
avmetadatamachinereadablecodeobject *metadataobject = metadataobjects.firstobject;
if ([[metadataobject type] isequaltostring:avmetadataobjecttypeqrcode]) {
nsstring *message = [metadataobject stringvalue];
[self.label performselectoronmainthread:@selector(settext:) withobject:message waituntildone:no];
}
}
}
|
需要提醒的是,由于avcapturemetadataoutput对象代理的设置,该代理方法会在setmetadataobjectsdelegate:queue:指定的队列上调用,如果需要更新用户界面,则必须在主线程中进行。
2 应用示例
下面,我们就做一个如下图所示的二维码阅读器:
其中主要实现的功能有:
-
通过摄像头实时扫描并读取二维码。
-
解析从相册中选择的二维码图片。
由于二维码的扫描是基于实时的视频捕获,因此相关的操作无法在模拟器上进行测试,也不能在没有相机的设备上进行测试。如果想要查看该应用,需要连接自己的iphone设备来运行。
2.1 创建项目
打开xcode,创建一个新的项目(file ewproject...),选择ios一栏下的application中的single view application模版,然后点击next,填写项目选项。在product name中填写qrcodereaderdemo,选择objective-c语言,点击next,选择文件位置,并单击create创建项目。
2.2 构建界面
打开main.storyboard文件,在当前控制器中嵌入导航控制器,并添加标题qr code reader:
在视图控制器中添加toolbar、flexible space bar button item、bar button item、view,布局如下:
其中,各元素及作用:
-
toolbar:添加在控制器视图的最底部,其bar item标题为start,具有双重作用,用于启动和停止扫描。
-
flexible space bar button item:分别添加在start的左右两侧,用于固定start 的位置使其居中显示。
-
bar button item:添加在导航栏的右侧,标题为album,用于从相册选择二维码图片进行解析。
-
view:添加在控制器视图的中间,用于稍后设置扫描框。在这里使用自动布局固定宽高均为260,并且水平和垂直方向都是居中。
创建一个名为scanview的新文件(file ewile…),它是uiview的子类。然后选中视图控制器中间添加的view,将该视图的类名更改为scanview:
打开辅助编辑器,将storyboard中的元素连接到代码中:
注意,需要在viewcontroller.m文件中导入scanview.h文件。
2.3 添加代码
2.3.1 扫描二维码
首先在viewcontroller.h文件中导入avfoundation框架:
1
|
#import <avfoundation/avfoundation.h>
|
切换到viewcontroller.m文件,添加avcapturemetadataoutputobjectsdelegate协议,并在接口部分添加下面的属性:
1
2
3
4
5
6
|
@interface viewcontroller ()<avcapturemetadataoutputobjectsdelegate>
// properties
@property (assign, nonatomic) bool isreading;
@property (strong, nonatomic) avcapturesession *capturesession;
@property (strong, nonatomic) avcapturevideopreviewlayer *previewlayer;
|
在viewdidload方法中添加下面代码:
1
2
3
4
5
6
7
|
- ( void )viewdidload
{
[super viewdidload];
self.isreading = no;
self.capturesession = nil;
}
|
然后在实现部分添加startscanning方法和stopscanning方法及相关代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
- ( void )startscanning
{
self.capturesession = [[avcapturesession alloc] init];
// add input
nserror *error;
avcapturedevice *device = [avcapturedevice defaultdevicewithmediatype:avmediatypevideo];
avcapturedeviceinput *deviceinput = [[avcapturedeviceinput alloc] initwithdevice:device error:&error];
if (!deviceinput) {
nslog(@ "%@" , [error localizeddescription]);
}
[self.capturesession addinput:deviceinput];
// add output
avcapturemetadataoutput *metadataoutput = [[avcapturemetadataoutput alloc] init];
[self.capturesession addoutput:metadataoutput];
// configure output
dispatch_queue_t queue = dispatch_queue_create( "myqueue" , null);
[metadataoutput setmetadataobjectsdelegate:self queue:queue];
[metadataoutput setmetadataobjecttypes:@[avmetadataobjecttypeqrcode]];
// configure previewlayer
self.previewlayer = [[avcapturevideopreviewlayer alloc] initwithsession:self.capturesession];
[self.previewlayer setvideogravity:avlayervideogravityresizeaspectfill];
[self.previewlayer setframe:self.view.bounds];
[self.view.layer addsublayer:self.previewlayer];
// start scanning
[self.capturesession startrunning];
}
- ( void )stopscanning
{
[self.capturesession stoprunning];
self.capturesession = nil;
[self.previewlayer removefromsuperlayer];
}
|
找到startstopaction:并在该方法中调用上面的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
- (ibaction)startstopaction:(id)sender
{
if (!self.isreading) {
[self startscanning];
[self.view bringsubviewtofront:self.toolbar];
[self.startstopbutton settitle:@ "stop" ];
}
else {
[self stopscanning];
[self.startstopbutton settitle:@ "start" ];
}
self.isreading = !self.isreading;
}
|
至此,二维码扫描相关的代码已经完成,如果想要它能够正常运行的话,还需要在info.plist文件中添加nscamerausagedescription键及相应描述以访问相机:
需要注意的是,现在只能扫描二维码但是还不能读取到二维码中的内容,不过我们可以连接设备,运行试下:
2.3.2 读取二维码
读取二维码需要实现avcapturemetadataoutputobjectsdelegate协议的captureoutput:didoutputmetadataobjects:fromconnection:方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
- ( void )captureoutput:(avcaptureoutput *)output didoutputmetadataobjects:(nsarray<__kindof avmetadataobject *> *)metadataobjects fromconnection:(avcaptureconnection *)connection
{
if (metadataobjects != nil && metadataobjects.count > 0) {
avmetadatamachinereadablecodeobject *metadataobject = metadataobjects.firstobject;
if ([[metadataobject type] isequaltostring:avmetadataobjecttypeqrcode]) {
nsstring *message = [metadataobject stringvalue];
[self performselectoronmainthread:@selector(displaymessage:) withobject:message waituntildone:no];
[self performselectoronmainthread:@selector(stopscanning) withobject:nil waituntildone:no];
[self.startstopbutton performselectoronmainthread:@selector(settitle:) withobject:@ "start" waituntildone:no];
self.isreading = no;
}
}
}
- ( void )displaymessage:(nsstring *)message
{
uiviewcontroller *vc = [[uiviewcontroller alloc] init];
uitextview *textview = [[uitextview alloc] initwithframe:vc.view.bounds];
[textview settext:message];
[textview setfont:[uifont preferredfontfortextstyle:uifonttextstylebody]];
textview.editable = no;
[vc.view addsubview:textview];
[self.navigationcontroller showviewcontroller:vc sender:nil];
}
|
在这里我们将扫码结果显示在一个新的视图中,如果你运行程序的话应该可以看到扫描的二维码内容了。
另外,为了使我们的应用更逼真,可以在扫描到二维码信息时让它播放声音。这首先需要在项目中添加一个音频文件:
然后在接口部分添加一个avaudioplayer对象的属性:
1
|
@property (strong, nonatomic) avaudioplayer *audioplayer;
|
在实现部分添加loadsound方法及代码,并在viewdidload中调用该方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
- ( void )loadsound
{
nsstring *soundfilepath = [[nsbundle mainbundle] pathforresource:@ "beep" oftype:@ "mp3" ];
nsurl *soundurl = [nsurl urlwithstring:soundfilepath];
nserror *error;
self.audioplayer = [[avaudioplayer alloc] initwithcontentsofurl:soundurl error:&error];
if (error) {
nslog(@ "could not play sound file." );
nslog(@ "%@" , [error localizeddescription]);
}
else {
[self.audioplayer preparetoplay];
}
}
- ( void )viewdidload
{
...
[self loadsound];
}
|
最后,在captureoutput:didoutputmetadataobjects:fromconnection:方法中添加下面的代码来播放声音:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
- ( void )captureoutput:(avcaptureoutput *)output didoutputmetadataobjects:(nsarray<__kindof avmetadataobject *> *)metadataobjects fromconnection:(avcaptureconnection *)connection
{
if (metadataobjects != nil && metadataobjects.count > 0) {
avmetadatamachinereadablecodeobject *metadataobject = metadataobjects.firstobject;
if ([[metadataobject type] isequaltostring:avmetadataobjecttypeqrcode]) {
...
self.isreading = no;
// play sound
if (self.audioplayer) {
[self.audioplayer play];
}
}
}
|
2.3.3 设置扫描框
目前点击start按钮,整个视图范围都可以扫描二维码。现在,我们需要设置一个扫描框,以限制只有扫描框区域内的二维码被读取。在这里,将扫描区域设置为storyboard中添加的视图,即scanview。
在实现部分找到startreading方法,添加下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
- ( void )startscanning
{
// configure previewlayer
...
// set the scanning area
[[nsnotificationcenter defaultcenter] addobserverforname:avcaptureinputportformatdescriptiondidchangenotification object:nil queue:[nsoperationqueue mainqueue] usingblock:^(nsnotification * _nonnull note) {
metadataoutput.rectofinterest = [self.previewlayer metadataoutputrectofinterestforrect:self.scanview.frame];
}];
// start scanning
...
}
|
需要注意的是,rectofinterest属性不能在设置 metadataoutput 时直接设置,而需要在avcaptureinputportformatdescriptiondidchangenotification通知里设置,否则 metadataoutputrectofinterestforrect:方法会返回 (0, 0, 0, 0)。
为了让扫描框更真实的显示,我们需要自定义scanview,为其绘制边框、四角以及扫描线。
首先打开scanview.m文件,在实现部分重写initwithcoder:方法,为scanview设置透明的背景颜色:
1
2
3
4
5
6
7
8
9
10
|
- (instancetype)initwithcoder:(nscoder *)adecoder
{
self = [super initwithcoder:adecoder];
if (self) {
self.backgroundcolor = [uicolor clearcolor];
}
return self;
}
|
然后重写drawrect:方法,为scanview绘制边框和四角:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
- ( void )drawrect:(cgrect)rect
{
cgcontextref context = uigraphicsgetcurrentcontext();
// 绘制白色边框
cgcontextaddrect(context, self.bounds);
cgcontextsetstrokecolorwithcolor(context, [uicolor whitecolor].cgcolor);
cgcontextsetlinewidth(context, 2.0);
cgcontextstrokepath(context);
// 绘制四角:
cgcontextsetstrokecolorwithcolor(context, [uicolor greencolor].cgcolor);
cgcontextsetlinewidth(context, 5.0);
// 左上角:
cgcontextmovetopoint(context, 0, 30);
cgcontextaddlinetopoint(context, 0, 0);
cgcontextaddlinetopoint(context, 30, 0);
cgcontextstrokepath(context);
// 右上角:
cgcontextmovetopoint(context, self.bounds.size.width - 30, 0);
cgcontextaddlinetopoint(context, self.bounds.size.width, 0);
cgcontextaddlinetopoint(context, self.bounds.size.width, 30);
cgcontextstrokepath(context);
// 右下角:
cgcontextmovetopoint(context, self.bounds.size.width, self.bounds.size.height - 30);
cgcontextaddlinetopoint(context, self.bounds.size.width, self.bounds.size.height);
cgcontextaddlinetopoint(context, self.bounds.size.width - 30, self.bounds.size.height);
cgcontextstrokepath(context);
// 左下角:
cgcontextmovetopoint(context, 30, self.bounds.size.height);
cgcontextaddlinetopoint(context, 0, self.bounds.size.height);
cgcontextaddlinetopoint(context, 0, self.bounds.size.height - 30);
cgcontextstrokepath(context);
}
|
如果希望在扫描过程中看到刚才绘制的扫描框,还需要切换到viewcontroller.m文件,在startstopaction:方法中添加下面的代码来显示扫描框:
1
2
3
4
5
6
7
8
9
10
|
- (ibaction)startstopaction:(id)sender
{
if (!self.isreading) {
...
[self.view bringsubviewtofront:self.toolbar]; // display toolbar
[self.view bringsubviewtofront:self.scanview]; // display scanview
...
}
...
}
|
现在运行,你会看到下面的效果:
接下来我们继续添加扫描线。
首先在scanview.h文件的接口部分声明一个nstimer对象的属性:
1
|
@property (nonatomic, strong) nstimer *timer;
|
然后切换到scanview.m文件,在实现部分添加loadscanline方法及代码,并在initwithcoder:方法中调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
- ( void )loadscanline
{
self.timer = [nstimer scheduledtimerwithtimeinterval:3.0 repeats:yes block:^(nstimer * _nonnull timer) {
uiview *lineview = [[uiview alloc] initwithframe:cgrectmake(0, 0, self.bounds.size.width, 1.0)];
lineview.backgroundcolor = [uicolor greencolor];
[self addsubview:lineview];
[uiview animatewithduration:3.0 animations:^{
lineview.frame = cgrectmake(0, self.bounds.size.height, self.bounds.size.width, 2.0);
} completion:^( bool finished) {
[lineview removefromsuperview];
}];
}];
}
- (instancetype)initwithcoder:(nscoder *)adecoder
{
...
if (self) {
...
[self loadscanline];
}
...
}
|
然后切换到viewcontroller.m文件,在startstopaction:方法中添加下面代码以启用和暂停计时器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
- (ibaction)startstopaction:(id)sender
{
if (!self.isreading) {
...
[self.view bringsubviewtofront:self.scanview]; // display scanview
self.scanview.timer.firedate = [nsdate distantpast]; //start timer
...
}
else {
[self stopscanning];
self.scanview.timer.firedate = [nsdate distantfuture]; //stop timer
...
}
...
}
|
最后,再在viewwillappear:的重写方法中添加下面代码:
1
2
3
4
5
6
|
- ( void )viewwillappear:( bool )animated
{
[super viewwillappear:animated];
self.scanview.timer.firedate = [nsdate distantfuture];
}
|
可以运行看下:
2.3.4 从图片解析二维码
从ios 8开始,可以使用core image框架中的cidetector解析图片中的二维码。在这个应用中,我们通过点击album按钮,从相册选取二维码来解析。
在写代码之前,需要在info.plist文件中添加nsphotolibraryaddusagedescription键及相应描述以访问相册:
然后在viewcontroller.m文件中添加uiimagepickercontrollerdelegate和uinavigationcontrollerdelegate协议:
@interface viewcontroller ()<avcapturemetadataoutputobjectsdelegate, uiimagepickercontrollerdelegate, uinavigationcontrollerdelegate>
在实现部分找到readingfromalbum:方法,添加下面代码以访问相册中的图片:
1
2
3
4
5
6
7
8
9
|
- (ibaction)readingfromalbum:(id)sender
{
uiimagepickercontroller *picker = [[uiimagepickercontroller alloc] init];
picker.delegate = self;
picker.sourcetype = uiimagepickercontrollersourcetypephotolibrary;
picker.allowsediting = yes;
[self presentviewcontroller:picker animated:yes completion:nil];
}
|
然后实现uiimagepickercontrollerdelegate的imagepickercontroller:didfinishpickingmediawithinfo:方法以解析选取的二维码图片:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
- ( void )imagepickercontroller:(uiimagepickercontroller *)picker didfinishpickingmediawithinfo:(nsdictionary<nsstring *,id> *)info
{
[picker dismissviewcontrolleranimated:yes completion:nil];
uiimage *selectedimage = [info objectforkey:uiimagepickercontrollereditedimage];
ciimage *ciimage = [[ciimage alloc] initwithimage:selectedimage];
cidetector *detector = [cidetector detectoroftype:cidetectortypeqrcode context:nil options:@{cidetectoraccuracy:cidetectoraccuracylow}];
nsarray *features = [detector featuresinimage:ciimage];
if (features.count > 0) {
ciqrcodefeature *feature = features.firstobject;
nsstring *message = feature.messagestring;
// display message
[self displaymessage:message];
// play sound
if (self.audioplayer) {
[self.audioplayer play];
}
}
}
|
现在可以运行试下从相册选取二维码来读取:
上图显示的是在模拟器中运行的结果。
至此,我们的二维码阅读器已经全部完成,如果需要完整代码,可以下载qrcodereaderdemo查看。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://www.jianshu.com/p/f3ed4f98590d