二维码(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