ios使用AVFoundation读取二维码的方法
二维码(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框架。
#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 应用示例
下面,我们就做一个如下图所示的二维码阅读器:
其中主要实现的功能有:
- 通过摄像头实时扫描并读取二维码。
- 解析从相册中选择的二维码图片。
由于二维码的扫描是基于实时的视频捕获,因此相关的操作无法在模拟器上进行测试,也不能在没有相机的设备上进行测试。如果想要查看该应用,需要连接自己的iphone设备来运行。
2.1 创建项目
打开xcode,创建一个新的项目(file\new\project...),选择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\new\file…),它是uiview的子类。然后选中视图控制器中间添加的view,将该视图的类名更改为scanview:
打开辅助编辑器,将storyboard中的元素连接到代码中:
注意,需要在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键及相应描述以访问相机:
需要注意的是,现在只能扫描二维码但是还不能读取到二维码中的内容,不过我们可以连接设备,运行试下:
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]; }
在这里我们将扫码结果显示在一个新的视图中,如果你运行程序的话应该可以看到扫描的二维码内容了。
另外,为了使我们的应用更逼真,可以在扫描到二维码信息时让它播放声音。这首先需要在项目中添加一个音频文件:
然后在接口部分添加一个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 ... } ... }
现在运行,你会看到下面的效果:
接下来我们继续添加扫描线。
首先在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]; }
可以运行看下:
2.3.4 从图片解析二维码
从ios 8开始,可以使用core image框架中的cidetector解析图片中的二维码。在这个应用中,我们通过点击album按钮,从相册选取二维码来解析。
在写代码之前,需要在info.plist文件中添加nsphotolibraryaddusagedescription键及相应描述以访问相册:
然后在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]; } } }
现在可以运行试下从相册选取二维码来读取:
上图显示的是在模拟器中运行的结果。
至此,我们的二维码阅读器已经全部完成,如果需要完整代码,可以下载qrcodereaderdemo查看。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。