媒体捕捉-iOS自定义二维码扫描功能

引言

随着iOS 7引入AV Foundation框架,二维码扫描功能已经成为iOS应用程序中不可或缺的一部分。现今,几乎每个应用都充分利用这一功能,为用户提供了诸如扫码登录、扫码填充等丰富多彩的便捷体验。这项技术不仅丰富了应用功能,也为开发人员的调试工作带来了极大便利。在本博客中,我们将深入探讨如何实现自定义二维码扫描,为您打开更广阔的应用开发可能性。

主要类-AVCaptureMetadataOutput

在二维码扫描中,我们仍然以AVCaptureSession为核心,配置输入(AVCaptureDeviceInput)和输出(AVCaptureOutput)。特别是在输出方面,我们使用AVCaptureMetadataOutput来专门处理摄像头捕获到的二维码、条形码等元数据。

通过设置metadataObjectTypes属性,我们可以灵活地指定要捕获的元数据类型,如二维码、QR码、条形码等。这样的设置允许我们精确控制扫描的类型,提高识别效率。

通过实现AVCaptureMetadataOutputObjectsDelegate代理,我们能够在识别到指定类型的元数据时触发相应的代理方法,从而处理这些元数据。这为开发者提供了处理扫描结果的机会,使得在应用中集成二维码扫描功能变得更为灵活和可定制。

//MARK:检测到指定类型元数据
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection{

}

功能实现

为了使描述更清晰,我们将二维码识别和元数据处理分别放置到两个不同的类里面来处理。

PHCameraController:主要负责启动会话,开启进行二维码识别。

PHPreviewView:我们在这个view里面来处理和呈现元数据。

PHCameraController

和其它媒体捕捉的功能几乎一样,配置会话,启动会话。不同的是会话的输出实现,另外我们还在里面定义了一个协议,当有二维码被识别到时会调用这个协议中的方法,将元数据传递给PHPreviewView。

接口:

下面看一下PHCameraController中接口的定义。

#import 
NS_ASSUME_NONNULL_BEGIN
@protocol PHCodeDetectionDelegate 
- (void)didDetectCodes:(NSArray *)codes;
@end
@interface PHCameraController : NSObject
@property(nonatomic,weak)id   codeDetectionDelegate;
@property(nonatomic,strong,readonly)AVCaptureSession * captureSession;

///设置会话
- (BOOL)setupSession:(NSError **)error;
///开始会话
- (void)startSession;
///停止会话
- (void)stopSession;

@end
NS_ASSUME_NONNULL_END

外漏了一个只读的AVCaptureSession和三个核心的方法。以及一个codeDetectionDelegate。

实现:

接下来看一下PHCameraController的实现,会相对复杂一点,但我们只需要将关注点放到配置会话输出以及AVCaptureMetadataOutputObjectsDelegate的代理方法上面。

设置会话:
#import "PHCameraController.h"
#import 
@interface PHCameraController ()
@property(nonatomic,strong)AVCaptureMetadataOutput * metadataOutput;
@end
@implementation PHCameraController

- (NSString *)sessionPreset{
    return AVCaptureSessionPreset640x480;
}

//MARK:设置会话
- (BOOL)setupSession:(NSError *__autoreleasing  _Nullable *)error{
    self.captureSession = [[AVCaptureSession alloc] init];
    self.captureSession.sessionPreset = self.sessionPreset;
    if (![self setupSessionInputs:error]) {
        return NO;
    }
    if (![self setupSessionOutputs:error]) {
        return NO;
    }
    return YES;
}
配置会话输入:

//MARK:配置会话输入
- (BOOL)setupSessionInputs:(NSError *__autoreleasing  _Nullable *)error{
    BOOL success = [super setupSessionInputs:error];
    if (success) {
        if (self.activeCamera.autoFocusRangeRestrictionSupported) {
            if ([self.activeCamera lockForConfiguration:error]) {
                self.activeCamera.autoFocusRangeRestriction = AVCaptureAutoFocusRangeRestrictionNear;
                [self.activeCamera unlockForConfiguration];
            }
        }
    }
    return success;
}

配置会话输出:
//MARK:配置会话输出
- (BOOL)setupSessionOutputs:(NSError *__autoreleasing  _Nullable *)error{
    self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
    if ([self.captureSession canAddOutput:self.metadataOutput]) {
        [self.captureSession addOutput:self.metadataOutput];
        NSArray * metadataObjectTypes = @[AVMetadataObjectTypeQRCode,AVMetadataObjectTypeAztecCode,AVMetadataObjectTypeUPCECode];
        self.metadataOutput.metadataObjectTypes = metadataObjectTypes;
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        [self.metadataOutput setMetadataObjectsDelegate:self queue:mainQueue];
        return YES;
    }else{
        return NO;
    }
}
识别到指定类型元数据的回调:
//MARK:检测到指定兴趣点的代理
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    [self.codeDetectionDelegate didDetectCodes:metadataObjects];
}

PHPreviewView

主要负责显示预览画面,及显示元数据内容。

接口:
#import 
#import 

NS_ASSUME_NONNULL_BEGIN

@interface PHPreviewView : UIView

@property(nonatomic,strong)AVCaptureSession * session;


@end

NS_ASSUME_NONNULL_END
实现:
initWithFrame:方法:
@interface PHPreviewView ()


@end

@implementation PHPreviewView

- (id)initWithFrame:(CGRect)frame{
    if ([super initWithFrame:frame]) {
        [self setupView];
    }
    return self;
}


- (void)setupView{
    self.codeLayers = @{}.mutableCopy;
    self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspect;
}

遵守了一个PHCodeDetectionDelegate协议。

设置预览layer的画面填充方式。

重写方法:

同样我们通过重写layerClass类方法来返回一个AVCaptureVideoPreviewLayer类。

通过重写session的setter来为AVCaptureVideoPreviewLayer设置session。

通过重写session的getter来返回当前session。

+ (Class)layerClass{
    return [AVCaptureVideoPreviewLayer class];
}

- (void)setSession:(AVCaptureSession *)session{
    [(AVCaptureVideoPreviewLayer*)self.layer setSession:session];
}

- (AVCaptureSession *)session{
    return [(AVCaptureVideoPreviewLayer*)self.layer session];
}
定义previewLayer的getter:
//MARK:当前显示画面layer
- (AVCaptureVideoPreviewLayer *)previewLayer{
    return (AVCaptureVideoPreviewLayer *)self.layer;
}
实现PHCodeDetectionDelegate代理:

//MARK:代理 检测到条码
- (void)didDetectCodes:(NSArray *)codes{
    if (codes.count <= 0) {
        return;
        
    }
    NSArray * transformedCodes = [self transformedCodesFromCodes:codes];
    for (AVMetadataMachineReadableCodeObject * code in transformedCodes) {
        NSString * stringValue = code.stringValue;
        if (self.codeArrowLayers.count == 0) {
            self.codeArrowLayers = [self makeCornersLayers];
        }
        for (int i = 0; i < self.codeArrowLayers.count; i ++) {
            if (i >= code.corners.count) {
                break;
            }
            CAShapeLayer * layer = self.codeArrowLayers[i];
            [self.previewLayer addSublayer:layer];
            layer.path = [self bezierPathWithCodes:code.corners index:i].CGPath;
        }
        NSLog(@"String :%@",stringValue);
    }
    [self.delegate didDetectCodes:codes];
}

关于这个方法我们需要分成几部分来理解。

首先调用了一个transformedCodesFromCodes:方法,只是我们自己定义的方法,在里面我们只是调用了AVCaptureVideoPreviewLayer提供的坐标转发方法将设备坐标空间元数据对象转换为视图坐标空间对象,实现如下:

//MARK:坐标转换
- (NSArray *)transformedCodesFromCodes:(NSArray *)codes{
    NSMutableArray * transformedCodes = [NSMutableArray array];
    for (AVMetadataObject * code in codes) {
        AVMetadataObject * transformedCode = [self.previewLayer transformedMetadataObjectForMetadataObject:code];
        [transformedCodes addObject:transformedCode];
    }
    return transformedCodes;
}

读取到的AVMetadataMachineReadableCodeObject对象里面会有一个bounds和corners两个数组,bounds属性提供了识别码的按坐标轴对其的矩形边界,corners属性提供角点字典表示的NSArray。后一个属性更实用,因为使用它可以让我们构建一个与条码的角点坐标紧密对其的Bezier路径。当然还有一个更重要的属性是stringValue也就是我们从二维码中读取到的内容。

示例中我们使用它的corners属性绘制出了二维码的顶点轮廓,实现如下:

创建轮廓layer:

//MARK: makeCornersLayers
- (NSArray *)makeCornersLayers{
    CAShapeLayer * leftTop = [[CAShapeLayer alloc] init];
    CAShapeLayer * leftBottom = [[CAShapeLayer alloc] init];
    CAShapeLayer * rightBottom = [[CAShapeLayer alloc] init];
    CAShapeLayer * rightTop = [[CAShapeLayer alloc] init];
    NSArray * array = @[leftTop,leftBottom,rightBottom,rightTop];
    for (CAShapeLayer * layer in array) {
        layer.strokeColor = [UIColor redColor].CGColor;
        layer.fillColor = [UIColor clearColor].CGColor;
        layer.lineWidth = 2.0;
    }
    return  array;
}

绘制轮廓path:

- (UIBezierPath *)bezierPathWithCodes:(NSArray *)codes index:(NSInteger)index{
    CGPoint point;
    CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)codes[index], &point);
    CGPoint prePoint = CGPointZero;
    CGPoint postPoint = CGPointZero;
    switch (index) {
        case 0:
        {
            prePoint = CGPointMake(point.x, point.y + 10);
            postPoint = CGPointMake(point.x + 10, point.y);
        }
            break;
        case 1:
        {
            prePoint = CGPointMake(point.x, point.y - 10);
            postPoint = CGPointMake(point.x + 10, point.y);
        }
            break;
        case 2:
        {
            prePoint = CGPointMake(point.x - 10, point.y);
            postPoint = CGPointMake(point.x, point.y - 10);
        }
            break;
        case 3:
        {
            prePoint = CGPointMake(point.x, point.y + 10);
            postPoint = CGPointMake(point.x - 10, point.y);
        }
            break;
    }
    UIBezierPath * path = [UIBezierPath bezierPath];
    [path moveToPoint:prePoint];
    [path addLineToPoint:point];
    [path addLineToPoint:postPoint];
    return path;
}

最后代理调用didDetectCodes:方法将结果传递到视图控制器,并停止会话。

视图控制器实现:

import "ViewController.h"
#import "PHPreviewView"
#import "PHCameraController.h"

@interface ViewController ()

///相机控制
@property(nonatomic,strong)PHCameraController * controller;
///预览view
@property(nonatomic,strong)PHPreviewView * previewView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupView];
}

- (void)setupView{
    [self addPreviewView];
    [self configController];
}

//MARK: 添加预览视图
- (void)addPreviewView{
    self.previewView = [[PHPreviewView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:self.previewView];
    self.previewView.delegate = self;
}

- (void)didDetectCodes:(NSArray *)codes{
    [self.controller stopSession];
}


- (BOOL)prefersStatusBarHidden{
    return YES;
}


@end

结语

当我们深入研究了上述简单的二维码扫描示例后,我们不禁会发现这只是冰山一角。iOS提供了丰富的功能和灵活的接口,让我们能够进一步深挖二维码扫描的世界。举例来说,我们可以定义扫描范围,通过调整参数来适应各种应用场景,从而提高扫描的精准度和效率。

此外,iOS还支持各种花哨的识别效果,可以为用户提供更为生动和愉悦的扫描体验。通过巧妙运用动画、声音等元素,我们能够使二维码扫描不仅仅是一项实用的功能,更是一种与用户互动的方式。

在这个不断创新和演变的移动应用时代,探索二维码扫描功能的可能性就像打开了一扇通往无限可能性的大门。无论是为了提升用户体验,还是为了创造独特的应用功能,二维码扫描都为开发者提供了丰富的创作空间。让我们在不断探索的道路上,发现更多精彩的可能性吧。

你可能感兴趣的:(媒体,二维码扫描)