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

时间:2022-12-07 21:54:03

二维码(quick response code,简称qr code)是由水平和垂直两个方向上的线条设计而成的一种二维条形码(barcode)。可以编码网址、电话号码、文本等内容,能够存储大量的数据信息。自ios 7以来,二维码的生成和读取只需要使用core image框架和avfoundation框架就能轻松实现。在这里,我们主要介绍二维码的读取。关于二维码的生成,可以查看使用cifilter生成二维码文章中的介绍。

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

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 应用示例

下面,我们就做一个如下图所示的二维码阅读器:

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

其中主要实现的功能有:

  1. 通过摄像头实时扫描并读取二维码。

  2. 解析从相册中选择的二维码图片。

由于二维码的扫描是基于实时的视频捕获,因此相关的操作无法在模拟器上进行测试,也不能在没有相机的设备上进行测试。如果想要查看该应用,需要连接自己的iphone设备来运行。

2.1 创建项目

打开xcode,创建一个新的项目(file ewproject...),选择ios一栏下的application中的single view application模版,然后点击next,填写项目选项。在product name中填写qrcodereaderdemo,选择objective-c语言,点击next,选择文件位置,并单击create创建项目。

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

2.2 构建界面

打开main.storyboard文件,在当前控制器中嵌入导航控制器,并添加标题qr code reader:

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

在视图控制器中添加toolbar、flexible space bar button item、bar button item、view,布局如下:

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

其中,各元素及作用:

  1. toolbar:添加在控制器视图的最底部,其bar item标题为start,具有双重作用,用于启动和停止扫描。

  2. flexible space bar button item:分别添加在start的左右两侧,用于固定start 的位置使其居中显示。

  3. bar button item:添加在导航栏的右侧,标题为album,用于从相册选择二维码图片进行解析。

  4. view:添加在控制器视图的中间,用于稍后设置扫描框。在这里使用自动布局固定宽高均为260,并且水平和垂直方向都是居中。

创建一个名为scanview的新文件(file ewile…),它是uiview的子类。然后选中视图控制器中间添加的view,将该视图的类名更改为scanview:

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

打开辅助编辑器,将storyboard中的元素连接到代码中:

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

注意,需要在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键及相应描述以访问相机:

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

需要注意的是,现在只能扫描二维码但是还不能读取到二维码中的内容,不过我们可以连接设备,运行试下:

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

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];
}

在这里我们将扫码结果显示在一个新的视图中,如果你运行程序的话应该可以看到扫描的二维码内容了。

另外,为了使我们的应用更逼真,可以在扫描到二维码信息时让它播放声音。这首先需要在项目中添加一个音频文件:

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

然后在接口部分添加一个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
  ...
 }
 ...
}

现在运行,你会看到下面的效果:

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

接下来我们继续添加扫描线。

首先在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];
}

可以运行看下:

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

2.3.4 从图片解析二维码

从ios 8开始,可以使用core image框架中的cidetector解析图片中的二维码。在这个应用中,我们通过点击album按钮,从相册选取二维码来解析。

在写代码之前,需要在info.plist文件中添加nsphotolibraryaddusagedescription键及相应描述以访问相册:

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

然后在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];
  }
 }
}

现在可以运行试下从相册选取二维码来读取:

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

上图显示的是在模拟器中运行的结果。

至此,我们的二维码阅读器已经全部完成,如果需要完整代码,可以下载qrcodereaderdemo查看。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://www.jianshu.com/p/f3ed4f98590d