在 iOS 中轻松扫描二维码与条形码
级别:★★☆☆☆
标签:「iOS 原生扫描」「AVCaptureSession」「AVCaptureDevice」「rectOfInterest」
作者: Xs·H
审校: QiShare团队
最近做IoT项目,在智能设备配网过程中有一个扫描设备或说明书上的二维码/条形码来读取设备信息的需求,要达到的效果大体如下:
想到几年前在帐号卫士中开发过扫码功能,就扒出来封装了一下(可以从QiQRCode中获取),以方便在项目中复用。
封装共包括QiCodeManager和QiCodePreviewView两个类。QiCodeManager负责扫描功能(二维码/条形码的识别和读取等),QiCodePreviewView负责扫描界面(扫码框、扫描线、提示语等)。可按照如下方式在项目中使用两个类。
// 初始化扫码界面
_previewView = [[QiCodePreviewView alloc] initWithFrame:self.view.bounds];
_previewView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
[self.view addSubview:_previewView];
// 初始化扫码管理类
__weak typeof(self) weakSelf = self;
_codeManager = [[QiCodeManager alloc] initWithPreviewView:_previewView completion:^{
// 开始扫描
[weakSelf.codeManager startScanningWithCallback:^(NSString * _Nonnull code) {} autoStop:YES];
}];
QiCodePreviewView内部使用CAShapeLayer绘制了遮罩maskLayer
、扫描框rectLayer
、框角标cornerLayer
和扫描线lineLayer
。因为此部分涉及代码较多,本文不做详解,可从QiQRCode中查看源码。关于CAShapeLayer的使用,QiShare在iOS 绘制圆角文章中有介绍到。
接下来重点介绍一下QiCodeManager中扫码功能的实现过程。
一、识别(捕捉)二维码/条形码
QiCodeManager是基于iOS 7+,对AVFoundation
框架中的AVCaptureSession
及相关类进行的封装。AVCaptureSession
是AVFoundation
框架中捕捉音视频等数据的核心类。要实现扫码功能,除了用到AVCaptureSession
之外,还要用到AVCaptureDevice
、AVCaptureDeviceInput
、AVCaptureMetadataOutput
和AVCaptureVideoPreviewLayer
。核心代码如下:
// input
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];
// output
AVCaptureMetadataOutput *output = [[AVCaptureMetadataOutput alloc] init];
[output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
// session
_session = [[AVCaptureSession alloc] init];
_session.sessionPreset = AVCaptureSessionPresetHigh;
if ([_session canAddInput:input]) {
[_session addInput:input];
}
if ([_session canAddOutput:output]) {
[_session addOutput:output];
// output在被add到session后才可设置metadataObjectTypes属性
output.metadataObjectTypes = @[AVMetadataObjectTypeQRCode, AVMetadataObjectTypeCode128Code, AVMetadataObjectTypeEAN13Code];
}
// previewLayer
AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session];
previewLayer.frame = previewView.layer.bounds;
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
[previewView.layer insertSublayer:previewLayer atIndex:0];
// AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
AVMetadataMachineReadableCodeObject *code = metadataObjects.firstObject;
if (code.stringValue) { }
}
以“面向人脑”的编程思想对上述代码进行解释:
1、我们需要使用AVCaptureVideoPreviewLayer的实例previewLayer
显示扫描二维码/条形码时看到的影像;
2、但是previewLayer
的初始化需要AVCaptureSession的实例session
对数据的输入输出进行控制;
3、那我们就初始化一个session
,并将输出流的质量设置为高质量AVCaptureSessionPresetHigh;
4、因为session
是依靠AVCaptureDeviceInput和AVCaptureMetadataOutput来控制数据输入输出的;
5、那就用AVCaptureDevice的实例device
初始化一个input
,指明device
为AVMediaTypeVideo类型;
6、再初始化一个output
,设置好delegate和queue以及所支持的元数据类型(二维码和不同格式的条形码);
7、然后将input
和output
添加到session
中就OK了,调用[session startRunning];就可以扫描二维码了;
8、最终从- captureOutput:didOutputMetadataObjects:fromConnection:
方法中得到捕捉到的二维码/条形码数据。
至此,在previewLayer范围内就可以识别二维码/条形码了。
二、指定识别二维码/条形码的区域
如果要控制在previewLayer的指定区域内识别二维码/条形码,可以通过修改output的rectOfInterest属性来达到目的。代码如下:
// 计算rect坐标
CGFloat y = rectFrame.origin.y;
CGFloat x = previewView.bounds.size.width - rectFrame.origin.x - rectFrame.size.width;
CGFloat h = rectFrame.size.height;
CGFloat w = rectFrame.size.width;
CGFloat rectY = y / previewView.bounds.size.height;
CGFloat rectX = x / previewView.bounds.size.width;
CGFloat rectH = h / previewView.bounds.size.height;
CGFloat rectW = w / previewView.bounds.size.width;
// 坐标赋值
output.rectOfInterest = CGRectMake(rectY, rectX, rectH, rectW);
1、上述的CGRectMake(rectY, rectX, rectH, rectW)与CGRectMake(x, y, w, h)的传统定义不同,可以将
rectOfInterest
理解成被翻转过的CGRect;
2、而rectY
,rectX
,rectH
,rectW
也不是控件或区域的值,而是所对应的比例,如上述代码中的计算公式,y, x, h, w的值可参考下图;
3、rectOfInterest
的默认值为CGRectMake(.0, .0, 1.0, 1.0),表示识别二维码/条形码的区域为全屏(previewLayer区域)。
PS: 其实iOS提供了官方API来将标准rect转换成
rectOfInterest
,但只有在[session startRunning]
之后调用才有效果,而且还会时不时地出现卡顿式地闪一下。代码如下:
// 可以在[session startRunning]之后用此语句设置扫码区域
metadataOutput.rectOfInterest = [previewLayer metadataOutputRectOfInterestForRect:rectFrame];
三、拉近二维码/条形码(放大视频内容)
当二维码/条形码离我们较远时,拉近二维码/条形码会是一个不错的功能,效果如下:
上述效果是使用双指缩放的方式来实现的,具体代码如下:
// 添加缩放手势
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinch:)];
[previewView addGestureRecognizer:pinchGesture];
- (void)pinch:(UIPinchGestureRecognizer *)gesture {
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// 设定有效缩放范围,防止超出范围而崩溃
CGFloat minZoomFactor = 1.0;
CGFloat maxZoomFactor = device.activeFormat.videoMaxZoomFactor;
if (@available(iOS 11.0, *)) {
minZoomFactor = device.minAvailableVideoZoomFactor;
maxZoomFactor = device.maxAvailableVideoZoomFactor;
}
static CGFloat lastZoomFactor = 1.0;
if (gesture.state == UIGestureRecognizerStateBegan) {
// 记录上次缩放的比例,本次缩放在上次的基础上叠加
lastZoomFactor = device.videoZoomFactor;// lastZoomFactor为外部变量
}
else if (gesture.state == UIGestureRecognizerStateChanged) {
CGFloat zoomFactor = lastZoomFactor * gesture.scale;
zoomFactor = fmaxf(fminf(zoomFactor, maxZoomFactor), minZoomFactor);
[device lockForConfiguration:nil];// 修改device属性之前须lock
device.videoZoomFactor = zoomFactor;// 修改device的视频缩放比例
[device unlockForConfiguration];// 修改device属性之后unlock
}
}
上述代码的核心逻辑比较简单:
1、在previewView
上添加一个双指捏合的手势pinchGesture
,并设定target和selector
;
2、在selector
方法中根据gesture.scale
调整device.videoZoomFactor
;
3、注意在修改device
属性之前要lock一下,修改完后unlock一下。
四、弱光环境下开启手电筒
弱光环境对扫码功能有较大的影响,通过监测光线亮度给用户提供打开手电筒的选择会提升不少的体验,如下图:
弱光监测的代码如下:
- (void)observeLightStatus:(void (^)(BOOL, BOOL))lightObserver {
_lightObserver = lightObserver;
AVCaptureVideoDataOutput *lightOutput = [[AVCaptureVideoDataOutput alloc] init];
[lightOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
if ([_session canAddOutput:lightOutput]) {
[_session addOutput:lightOutput];
}
}
// AVCaptureVideoDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
// 通过sampleBuffer获取到光线亮度值brightness
CFDictionaryRef metadataDicRef = CMCopyDictionaryOfAttachments(NULL, sampleBuffer, kCMAttachmentMode_ShouldPropagate);
NSDictionary *metadataDic = (__bridge NSDictionary *)metadataDicRef;
CFRelease(metadataDicRef);
NSDictionary *exifDic = metadataDic[(__bridge NSString *)kCGImagePropertyExifDictionary];
CGFloat brightness = [exifDic[(__bridge NSString *)kCGImagePropertyExifBrightnessValue] floatValue];
// 初始化一些变量,作为是否透传brightness的因数
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
BOOL torchOn = device.torchMode == AVCaptureTorchModeOn;
BOOL dimmed = brightness < 1.0;
static BOOL lastDimmed = NO;
// 控制透传逻辑:第一次监测到光线或者光线明暗变化(dimmed变化)时透传
if (_lightObserver) {
if (!_lightObserverHasCalled) {
_lightObserver(dimmed, torchOn);
_lightObserverHasCalled = YES;
lastDimmed = dimmed;
}
else if (dimmed != lastDimmed) {
_lightObserver(dimmed, torchOn);
lastDimmed = dimmed;
}
}
}
弱光监测是依赖AVCaptureVideoDataOutput和AVCaptureVideoDataOutputSampleBufferDelegate来实现的。
1、初始化AVCaptureVideoDataOutput的实例lightOutput
后,设定delegate并将lightOutput
添加到session
中;
2、实现AVCaptureVideoDataOutputSampleBufferDelegate的回调方法-captureOutput:didOutputSampleBuffer:fromConnection:
;
3、对回调方法中的sampleBuffer
进行各种操作(具体参考上述代码细节),并最终获取到光线亮度brightness
;
4、根据brightness
的值设定弱光的标准以及是否透传给业务逻辑(这里认为brightness < 1.0
为弱光)。
调用- observeLightStatus:
方法并实现blck即可接收透传过来的光线状态和手电筒状态,并根据状态对UI做相应的调整,代码如下:
__weak typeof(self) weakSelf = self;
[self observeLightStatus:^(BOOL dimmed, BOOL torchOn) {
if (dimmed || torchOn) {// 变为弱光或者手电筒处于开启状态
[weakSelf.previewView stopScanning];// 停止扫描动画
[weakSelf.previewView showTorchSwitch];// 显示手电筒开关
} else {// 变为亮光并且手电筒处于关闭状态
[weakSelf.previewView startScanning];// 开始扫描动画
[weakSelf.previewView hideTorchSwitch];// 隐藏手电筒开关
}
}];
当出现手电筒开关时,我们可以通过点击开关改变手电筒的状态。开关手电筒的代码如下:
+ (void)switchTorch:(BOOL)on {
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureTorchMode torchMode = on? AVCaptureTorchModeOn: AVCaptureTorchModeOff;
if (device.hasFlash && device.hasTorch && torchMode != device.torchMode) {
[device lockForConfiguration:nil];// 修改device属性之前须lock
[device setTorchMode:torchMode];// 修改device的手电筒状态
[device unlockForConfiguration];// 修改device属性之后unlock
}
}
手电筒开关(按钮)封装在QiCodePreviewView中,QiCodeManager中通过QiCodePreviewViewDelegate的-codeScanningView:didClickedTorchSwitch:
方法获取手电筒开关的点击事件,并做相应的逻辑处理。代码如下:
// QiCodePreviewViewDelegate
- (void)codeScanningView:(QiCodePreviewView *)scanningView didClickedTorchSwitch:(UIButton *)switchButton {
switchButton.selected = !switchButton.selected;
[QiCodeManager switchTorch:switchButton.selected];
_lightObserverHasCalled = switchButton.selected;
}
综上,扫描二维码/条形码的功能就实现完了。此外,QiCodeManager中还封装了生成二维码/条形码的方法,下篇文章介绍。
示例源码:QiQRCode可从GitHub的QiShare开源库中获取。
推荐文章:
iOS 了解Xcode Bitcode
iOS 重绘之drawRect
iOS 编写高质量Objective-C代码(八)
iOS KVC与KVO简介
推荐阅读
-
微信 "扫一扫 "物联网,全面揭秘 "扫一扫 "背后的扫盲技术!-1.1 扫一扫感知物体是做什么的? 1.1 微信扫一扫是做什么的? 扫一扫识物是指以图片或视频(商品图片:鞋/包/美妆/服饰/家电/玩具/图书/食品/珠宝/家具/其他商品)为输入媒介,挖掘微信内容生态中的有价值信息(电商+百科+资讯,如图1所示),并展示给用户。这里的电商基本涵盖了微信小程序覆盖上亿SKU的全量优质电商,可以支持用户货比N家并直接下单购买,百科和资讯则聚合了微信内的头部自媒体如搜狗、搜搜、百度等,向用户展示和分享拍摄商品相关的内容资讯。 图 1 扫一扫识别功能示意图 欢迎大家更新iOS新版微信→扫一扫→识货,亲自体验,也欢迎大家通过识货界面的反馈按钮向我们提交反馈意见。 扫一扫识物实景图展示 1.2 扫一扫识物有哪些使用场景? 扫一扫识物的目的是为用户访问微信内部生态内容开辟一个新窗口,以用户扫图片为输入形式,为用户提供微信生态内容中的百科、资讯、电商等作为展示页面。除了用户熟悉的扫一扫操作外,我们还将进一步拓展长按操作,让用户更方便地进行扫一扫操作。"扫一扫知事 "的落地场景主要涵盖三大部分: a. 科普知识: a.科普知识。用户通过扫一扫,可以在微信生态圈中获取该对象的百科、资讯等常识或趣闻,帮助用户更好地了解该对象; b.购物场景。同样的搜索功能支持用户看到喜欢的商品立即检索到微信小程序电商中的同款商品,支持用户即扫即购; c.广告场景。扫一扫识别物体可以辅助公众号文章、视频更好地理解其中蕴含的图片信息,从而更好地投放匹配广告,提高点击率。 1.3 Sweep Sense 为 Sweep 家族带来了哪些新技术? 对于扫一扫来说,大家耳熟能详的应该就是扫一扫二维码、扫一扫小程序码、扫一扫条形码、扫一扫翻译了。无论是各种形式的编码还是文字字符,都可以看作是图片的一种特定编码形式,而物的识别则是对自然场景图片的识别,这对于扫一扫家族来说是一个质的飞跃,我们希望从物的识别入手,进一步拓展扫一扫对自然场景图片的理解能力,比如扫酒、扫车、扫植物、扫人脸等服务,如下图3所示。 图 3 Sweep 家族
-
手把手教你轻松掌握iOS设备上的二维码生成与扫描详解
-
轻松实现 Android: 一站式二维码与条形码扫描、生成与识别
-
Flutter实战教程:快速掌握Flutter中的二维码与条形码扫描功能实现
-
在H5网页中轻松实现扫描二维码的功能指南
-
使用ZXing在Android上轻松创建与扫描二维码指南
-
在 iOS 中轻松扫描二维码与条形码
-
使用Qt进行三方库开发:轻松实现二维码生成与识别,以及条形码扫描与解析
-
轻松实现:在 Android 中整合 zxing,快速识别与生成图片二维码
-
探究H5纯网页在非微信环境中通过扫描二维码实现的功能实践与探讨