iOS系统原生二维码扫描开发
项目背景
最近刚从成都回到大连的一家做对日开发的公司, 项目需求是维护既存的业务逻辑, 修改API , 以及三方库更新
业务主体就是利用ZXing实现的二维码扫描, 存本地后批量上传.
因为是2015年的项目, 因此, 之前的ZXing代码库在iOS10 , 11 的系统上无法运行
ZXing_Bug.png
需要的功能
- 扫描识别各种类型的码 (条形码, 二维码 , 彩色码 等)
- 开关闪光灯
- 识别本地图片
方案
- 更新ZXing库为ZXingObjC库
- 使用系统原生代码进行开发
(据传言, 有的二维码系统原生的识别不出来, 但是ZXingObjC 可以识别 , 具体真假, 有待考证, 暂时没有碰到)
下面我们就来分别对这两种方案进行实现:
系统原生代码
1. 扫描识别
首先导入库文件<AVFoundation/AVFoundation.h>
#import <AVFoundation/AVFoundation.h>
接着签订需要的代理, 并创建所需要的属性(这里我只讲述关键代码)
AVCaptureMetadataOutputObjectsDelegate : 用于扫描获取到数据后的回调 , (metadataObjects: 扫描二维码数据信息)
AVCaptureVideoDataOutputSampleBufferDelegate : 根据光线强弱值打开手电筒的方法 , (brightnessValue: 光线强弱值)
@interface LYCodeScanManager () <AVCaptureMetadataOutputObjectsDelegate, AVCaptureVideoDataOutputSampleBufferDelegate>
@property (nonatomic, strong) AVCaptureSession *session;
@property (nonatomic, strong) AVCaptureVideoDataOutput *videoDataOutput;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewLayer;
@end
准备工作做完 , 我们就搞起来吧
// 1、获取摄像设备
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// 2、创建摄像设备输入流
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:nil];
// 3、创建元数据输出流
AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init];
[metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
// 设置扫描范围(每一个取值0~1,以屏幕右上角为坐标原点)
// 注:微信二维码的扫描范围是整个屏幕,这里并没有做处理(可不用设置);
// 如需限制扫描框范围,打开下一句注释代码并进行相应调整
// metadataOutput.rectOfInterest = CGRectMake(0.05, 0.2, 0.7, 0.6);
// 4、创建会话对象
_session = [[AVCaptureSession alloc] init];
// 并设置会话采集率
_session.sessionPreset = AVCaptureSessionPreset1920x1080;
// 5、添加元数据输出流到会话对象
[_session addOutput:metadataOutput];
// 创建摄像数据输出流并将其添加到会话对象上, --> 用于识别光线强弱
self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
[_videoDataOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
[_session addOutput:_videoDataOutput];
// 6、添加摄像设备输入流到会话对象
[_session addInput:deviceInput];
// 7、设置数据输出类型(如下设置为条形码和二维码兼容),需要将数据输出添加到会话后,才能指定元数据类型,否则会报错
metadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode, AVMetadataObjectTypeEAN13Code, AVMetadataObjectTypeEAN8Code, AVMetadataObjectTypeCode128Code];
// 8、实例化预览图层, 用于显示会话对象
_videoPreviewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session];
// 保持纵横比;填充层边界
_videoPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
CGFloat x = 0;
CGFloat y = 0;
CGFloat w = [UIScreen mainScreen].bounds.size.width;
CGFloat h = [UIScreen mainScreen].bounds.size.height;
_videoPreviewLayer.frame = CGRectMake(x, y, w, h);
[currentController.view.layer insertSublayer:_videoPreviewLayer atIndex:0];
// 9、启动会话
[_session startRunning];
当然, 不要忘了在页面销毁或者你不需要用它的时候, 一定要先让他停下来 : [_session stopRunning] 即可
最后, 我们只需要在delegate回调方法中, 获取识别出的数据就可以了
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
NSLog(@"metadataObjects - - %@", metadataObjects);
if (metadataObjects != nil && metadataObjects.count > 0) {
AVMetadataMachineReadableCodeObject *obj = metadataObjects[0];
NSLog(@"%@",[obj stringValue]);
} else {
NSLog(@"暂未识别出扫描的二维码");
}
}
这样我们就把扫描出的数据打印出来了
2. 开关闪光灯
刚才 , 我们在创建会话对象时 , 已经把摄像设备的输入流和输出流添加上了, 因此我们这边直接调用AVCaptureVideoDataOutputSampleBufferDelegate的回调方法即可获取到光线的强弱值 , 从而可以控制是否需要闪光灯
(这里创建了一个按钮lightBtn , 如果需要, 则添加上去, 不需要则移除)
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
// 这个方法会时时调用,但内存很稳定
CFDictionaryRef metadataDict = CMCopyDictionaryOfAttachments(NULL,sampleBuffer, kCMAttachmentMode_ShouldPropagate);
NSDictionary *metadata = [[NSMutableDictionary alloc] initWithDictionary:(__bridge NSDictionary*)metadataDict];
CFRelease(metadataDict);
NSDictionary *exifMetadata = [[metadata objectForKey:(NSString *)kCGImagePropertyExifDictionary] mutableCopy];
float brightnessValue = [[exifMetadata objectForKey:(NSString *)kCGImagePropertyExifBrightnessValue] floatValue];
NSLog(@"%f",brightnessValue);
if (brightnessValue < - 1) {
[self.view addSubview:self.lightBtn];
} else {
if (self.isSelectedFlashlightBtn == NO) {
[self removeFlashlightBtn];
}
}
}
lightBtn 点击方法 , 控制开关闪光灯
- (void)lightBtnAction:(UIButton *)button {
if (button.selected == NO) {
/** 打开手电筒 */
AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error = nil;
if ([captureDevice hasTorch]) {
BOOL locked = [captureDevice lockForConfiguration:&error];
if (locked) {
captureDevice.torchMode = AVCaptureTorchModeOn;
[captureDevice unlockForConfiguration];
}
}
self.isSelectedFlashlightBtn = YES;
button.selected = YES;
} else {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
if ([device hasTorch]) {
[device lockForConfiguration:nil];
[device setTorchMode: AVCaptureTorchModeOff];
[device unlockForConfiguration];
}
self.isSelectedFlashlightBtn = NO;
self.flashlightBtn.selected = NO;
[self.flashlightBtn removeFromSuperview];
});
}
}
本地图片识别
首先就是当前controller 进入相册页面获取图片, 正常的签订 <UINavigationControllerDelegate, UIImagePickerControllerDelegate> 协议, 然后push 就好了
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
imagePicker.delegate = self;
[self presentViewController:imagePicker animated:YES completion:nil];
定义一个方法用于返回一个不超过屏幕尺寸的Image
/// 返回一张不超过屏幕尺寸的 image
+ (UIImage *)LY_imageSizeWithScreenImage:(UIImage *)image {
CGFloat imageWidth = image.size.width;
CGFloat imageHeight = image.size.height;
CGFloat screenWidth = SGQRCodeScreenWidth;
CGFloat screenHeight = SGQRCodeScreenHeight;
if (imageWidth <= screenWidth && imageHeight <= screenHeight) {
return image;
}
CGFloat max = MAX(imageWidth, imageHeight);
CGFloat scale = max / (screenHeight * 2.0);
CGSize size = CGSizeMake(imageWidth / scale, imageHeight / scale);
UIGraphicsBeginImageContext(size);
[image drawInRect:CGRectMake(0, 0, size.width, size.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
回调方法中调用对图片进行处理
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
// 对选取照片的处理,如果选取的图片尺寸过大,则压缩选取图片,否则不作处理
UIImage *image = [self LY_imageSizeWithScreenImage:info[UIImagePickerControllerOriginalImage]];
// CIDetector(CIDetector可用于人脸识别)进行图片解析,从而使我们可以便捷的从相册中获取到二维码
// 声明一个 CIDetector,并设定识别类型 CIDetectorTypeQRCode
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{CIDetectorAccuracy: CIDetectorAccuracyHigh}];
// 取得识别结果
NSArray *features = [detector featuresInImage:[CIImage imageWithCGImage:image.CGImage]];
if (features.count == 0) {
if (self.isOpenLog) {
if (self.delegate && [self.delegate respondsToSelector:@selector(QRCodeAlbumManagerDidReadQRCodeFailure:)]) {
[self.delegate QRCodeAlbumManagerDidReadQRCodeFailure:self];
}
}
[self.currentVC dismissViewControllerAnimated:YES completion:nil];
return;
} else {
for (int index = 0; index < [features count]; index ++) {
CIQRCodeFeature *feature = [features objectAtIndex:index];
NSString *resultStr = feature.messageString;
NSLog(@"相册中读取二维码数据信息 - - %@", resultStr);
self.detectorString = resultStr;
}
[self.currentVC dismissViewControllerAnimated:YES completion:^{
}
}
总结 : 总的来说, 还是一个相对比较简单的需求, 建议写个manager去控制, 不然代码太多, 输出类型的地方建议不要把所有的都写上, 会导致识别不出来, 亲测有效 ... 哈哈, 所以需要用什么就写什么就好了