欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2023-12-19 16:42:40
二维码(quick response code,简称qr code)是由水平和垂直两个方向上的线条设计而成的一种二维条形码(barcode)。可以编码网址、电话号码、文本等...

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

#import <avfoundation/avfoundation.h>

2、创建一个avcapturesession对象。

avcapturesession *capturesession = [[avcapturesession alloc] init];

3、为avcapturesession对象添加输入和输出。

// 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对象,主要是设置代理和要处理的元数据对象类型。

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对象来显示捕获到的视频。

avcapturevideopreviewlayer *previewlayer = [[avcapturevideopreviewlayer alloc] initwithsession:capturesession];
[previewlayer setvideogravity:avlayervideogravityresizeaspectfill];
[previewlayer setframe:self.view.bounds];
[self.view.layer addsublayer:previewlayer];

6、给avcapturesession对象发送startrunning消息以启动视频捕获。

[capturesession startrunning];

7、实现avcapturemetadataoutputobjectsdelegate的captureoutput:didoutputmetadataobjects:fromconnection:方法来处理捕获到的元数据,并将其读取出来。

- (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\new\project...),选择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\new\file…),它是uiview的子类。然后选中视图控制器中间添加的view,将该视图的类名更改为scanview:

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

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

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

注意,需要在viewcontroller.m文件中导入scanview.h文件。

2.3 添加代码

2.3.1 扫描二维码

首先在viewcontroller.h文件中导入avfoundation框架:

#import <avfoundation/avfoundation.h>

切换到viewcontroller.m文件,添加avcapturemetadataoutputobjectsdelegate协议,并在接口部分添加下面的属性:

@interface viewcontroller ()<avcapturemetadataoutputobjectsdelegate>

// properties
@property (assign, nonatomic) bool isreading;
@property (strong, nonatomic) avcapturesession *capturesession;
@property (strong, nonatomic) avcapturevideopreviewlayer *previewlayer;

在viewdidload方法中添加下面代码:

- (void)viewdidload
{
 [super viewdidload];
 
 self.isreading = no;
 self.capturesession = nil;
}

然后在实现部分添加startscanning方法和stopscanning方法及相关代码:

- (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:并在该方法中调用上面的方法:

- (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:方法:

- (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对象的属性:

@property (strong, nonatomic) avaudioplayer *audioplayer;

在实现部分添加loadsound方法及代码,并在viewdidload中调用该方法:

- (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:方法中添加下面的代码来播放声音:

- (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方法,添加下面的代码:

- (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设置透明的背景颜色:

- (instancetype)initwithcoder:(nscoder *)adecoder
{
 self = [super initwithcoder:adecoder];
 
 if (self) {
  self.backgroundcolor = [uicolor clearcolor];
 }
 
 return self;
}

然后重写drawrect:方法,为scanview绘制边框和四角:

- (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:方法中添加下面的代码来显示扫描框:

- (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对象的属性:

@property (nonatomic, strong) nstimer *timer;

然后切换到scanview.m文件,在实现部分添加loadscanline方法及代码,并在initwithcoder:方法中调用:

- (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:方法中添加下面代码以启用和暂停计时器:

- (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:的重写方法中添加下面代码:

- (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:方法,添加下面代码以访问相册中的图片:

- (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:方法以解析选取的二维码图片:

- (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查看。

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

上一篇:

下一篇: