iOS開發之掃描二維碼

YungFan發表於2017-12-13

自iOS7以後,iOS掃描二維碼不需要藉助於第三方框架了,蘋果在AVFoundation中原生支援了掃描二維碼的API,主要涉及到5個類,這5個類在自定義相機或者視訊時也用得上,網上有很多介紹,這5個類分別為:

AVCaptureSession:媒體捕獲會話,負責把捕獲的音視訊資料輸出到輸出裝置中。

AVCaptureDevice:輸入裝置,如麥克風、攝像頭。

AVCaptureDeviceInput:裝置輸入資料管理物件,可以根據AVCaptureDevice建立對應的AVCaptureDeviceInput物件,該物件將會被新增到AVCaptureSession中管理。

AVCaptureOutput:輸出資料管理物件,用於接收各類輸出資料,有很多子類,每個子類用途都不一樣,該物件將會被新增到AVCaptureSession中管理。

AVCaptureVideoPreviewLayer:相機拍攝預覽圖層,是CALayer的子類,使用該物件可以實時檢視拍照或視訊錄製效果,設定好尺寸後需要新增到父view的layer中。
複製程式碼

我在參考了網上的很多部落格並自己摸索了以後,寫了一個具體的實現案例,過程中遇到很多坑,在此記錄並分享一下。

執行環境:Xcode 8.3.2 + iOS 8. 4真機、iOS 10.3.1真機

一、核心步驟:

1、建立AVCaptureSession會話 2、建立AVCaptureDevice裝置 3、建立輸入AVCaptureDeviceInput與輸出裝置AVCaptureMetadataOutput,並新增到上面的會話中 4、建立預覽層 5、設定掃描區域

二、實現

從上面的描述看,除了預覽層,其他的和UI介面似乎沒什麼關係,但是實際開發中,掃描介面一般都是設計的比較人性化的,如支付寶、微信等,中間都有一個小框,有個線上下掃,這個其實就是用UI來配合掃描二維碼,給使用者一種好的體驗。

1、介面佈局

介面佈局.png

2、主要程式碼

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface ViewController () <AVCaptureMetadataOutputObjectsDelegate, CALayerDelegate>

/**
 *  UI
 */
@property (weak, nonatomic) IBOutlet UIView *scanView;
@property (weak, nonatomic) IBOutlet UIImageView *scanline;
@property (weak, nonatomic) IBOutlet UILabel *result;

/**
 *  掃描區域的高度約束值(寬度一致)
 */
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *scanViewH;
/**
 *  掃描線的頂部約束值
 */
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *scanlineTop;
/**
 *  掃描線的高度
 */
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *scanlineH;
@property(nonatomic, strong) CALayer *maskLayer;

/**
 *  五個類
 */
@property(nonatomic, strong) AVCaptureDevice *device;

@property(nonatomic, strong)  AVCaptureDeviceInput *input;

@property(nonatomic, strong)  AVCaptureMetadataOutput *output;

@property(nonatomic, strong) AVCaptureSession *session;

@property(nonatomic, strong) AVCaptureVideoPreviewLayer *layer;

@end

@implementation ViewController

#pragma mark - 懶載入

-(AVCaptureDevice *)device{
    if (_device == nil) {
        
        _device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
        
    }
    return _device;
    
}

-(AVCaptureDeviceInput *)input{
    
    if (_input == nil) {
        
        _input = [AVCaptureDeviceInput deviceInputWithDevice:self.device error:nil];
        
    }
    
    return  _input;
    
}

-(AVCaptureMetadataOutput *)output{
    
    if (_output == nil) {
        
        _output = [[AVCaptureMetadataOutput alloc]init];
        
        [_output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
        
    }
    
    return  _output;
}

#pragma mark - ViewController生命週期
/**
 *  執行掃描動畫
 */
-(void)viewDidAppear:(BOOL)animated{
    
    [super viewDidAppear:animated];
    
    [self startAnim];
    
}

/**
 *  註冊進入前臺通知 保證下次進來還有掃描動畫
 */
-(void)viewWillAppear:(BOOL)animated{
    
    [super viewWillAppear:animated];

    //註冊程式進入前臺通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector (startAnim) name: UIApplicationWillEnterForegroundNotification object:nil];
}

/**
 *  移除通知
 */
-(void)viewWillDisappear:(BOOL)animated{
    
    [super viewWillDisappear:animated];
    //解除程式進入前臺通知
    [[NSNotificationCenter defaultCenter] removeObserver:self name: UIApplicationWillEnterForegroundNotification object:nil];
     
     
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    //1、建立會話
    AVCaptureSession *session = [[AVCaptureSession alloc]init];
    
    if ([session canSetSessionPreset:AVCaptureSessionPresetHigh]) {
        
        [session setSessionPreset:AVCaptureSessionPresetHigh];
        
    }
    
    //2、新增輸入和輸出裝置
    if([session canAddInput:self.input]){
        
        [session addInput:self.input];
        
    }
    if([session canAddOutput:self.output]){
        
        [session addOutput:self.output];
    }
    
    
    //3、設定這次掃描的資料型別
    self.output.metadataObjectTypes = self.output.availableMetadataObjectTypes;
    
    //4、建立預覽層
    AVCaptureVideoPreviewLayer *layer = [AVCaptureVideoPreviewLayer layerWithSession:session];
    
    layer.frame = self.view.bounds;
    
    [self.view.layer insertSublayer:layer atIndex:0];
    
    
    //5、建立周圍的遮罩層
    CALayer *maskLayer = [[CALayer alloc]init];
    
    maskLayer.frame = self.view.bounds;
    
    //此時設定的顏色就是中間掃描區域最終的顏色
    maskLayer.backgroundColor = [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:0.2].CGColor;
    
    maskLayer.delegate = self;
    
    [self.view.layer insertSublayer:maskLayer above:layer];
    
    //讓代理方法呼叫 將周圍的蒙版顏色加深
    [maskLayer setNeedsDisplay];
    
    
    //6、關鍵設定掃描的區域 方法一:自己計算
    //    CGFloat x = (self.view.bounds.size.width - self.scanViewH.constant) * 0.5;
    //
    //    CGFloat y = (self.view.bounds.size.height- self.scanViewH.constant) * 0.5;
    //
    //    CGFloat w = self.scanViewH.constant;
    //
    //    CGFloat h = w;
    //
    //
    //    self.output.rectOfInterest = CGRectMake(y/self.view.bounds.size.height, x/self.view.bounds.size.width,  h/self.view.bounds.size.height, w/self.view.bounds.size.width);
    
    //6、關鍵設定掃描的區域,方法二:直接轉換,但是要在 AVCaptureInputPortFormatDescriptionDidChangeNotification 通知裡設定,否則 metadataOutputRectOfInterestForRect: 轉換方法會返回 (0, 0, 0, 0)。
    
    __weak __typeof(&*self)weakSelf = self;
    
    [[NSNotificationCenter defaultCenter] addObserverForName:AVCaptureInputPortFormatDescriptionDidChangeNotification object:nil queue:[NSOperationQueue currentQueue] usingBlock: ^(NSNotification *_Nonnull note) {
        
        weakSelf.output.rectOfInterest = [weakSelf.layer metadataOutputRectOfInterestForRect:self.scanView.frame];
        
    }];
    
    
    //7、開始掃描
    [session startRunning];
    
    
    self.session = session;
    self.layer = layer;
    self.maskLayer = maskLayer;
}

-(void)dealloc{
    
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    
}


#pragma mark - 代理方法
/**
 *  如果掃描到了二維碼 回撥該代理方法
 */
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    
    if(metadataObjects.count > 0 && metadataObjects != nil){
        
        
        AVMetadataMachineReadableCodeObject *metadataObject = [metadataObjects lastObject];
        
        NSString *result = metadataObject.stringValue;
        
        self.result.text = result;
        
        [self.session stopRunning];
        
        [self.scanline removeFromSuperview];
        
    }
    
    
}

/**
 *   蒙版中間一塊要空出來
 */

-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    
    if (layer == self.maskLayer) {
        
        UIGraphicsBeginImageContextWithOptions(self.maskLayer.frame.size, NO, 1.0);
        
        //蒙版新顏色
        CGContextSetFillColorWithColor(ctx, [UIColor colorWithRed:0.1 green:0.1 blue:0.1 alpha:0.8].CGColor);
        
        CGContextFillRect(ctx, self.maskLayer.frame);
        
        //轉換座標
        CGRect scanFrame = [self.view convertRect:self.scanView.frame fromView:self.scanView.superview];
        
        //空出中間一塊
        CGContextClearRect(ctx, scanFrame);
    }
}

@end

#pragma mark - 自定義方法
/**
 *  掃描的那條線動起來
 */
-(void)startAnim{
    
    //如果是第二次進來 那麼動畫已經執行完畢 要重新開始動畫的話 必須讓約束歸位
    if(self.scanlineTop.constant == self.scanViewH.constant - 4){
    
        self.scanlineTop.constant -= self.scanViewH.constant - 4;
    
        [self.view layoutIfNeeded];
    
    }
    
    //執行動畫
    [UIView animateWithDuration:3.0 delay:0 options:UIViewAnimationOptionRepeat animations:^{

        self.scanlineTop.constant = self.scanViewH.constant - 4;
        
        [self.view layoutIfNeeded];
        
        
    } completion:nil];
    
}
複製程式碼

Info.plist中 不同iOS版本需要新增相應的許可權

三、最終效果

掃描二維碼.gif

總結

一、遇到的坑

1、設定了AutoLayout,想要做動畫,這時候動畫放在viewDidAppear中執行,並且不要用bounds,frame來改變動畫,要用具體的約束,但是直接在UIView動畫中修改約束是沒效果的,需要在設定完約束以後,加上[self.view layoutIfNeeded];

2、設定掃描區域,也就是設定AVCaptureMetadataOutputrectOfInterest屬性,它是一個CGRect型別,但是它的四個值和傳統的不一樣,是**(y,x,高,寬)**且是比例值,取值範圍為0~1。那麼有兩種方案,第一種需要自己計算具體位置的比例,如程式碼中註釋的那些。第二種方案用AVCaptureVideoPreviewLayermetadataOutputRectOfInterestForRect方法,但是直接設定是沒有效果的,必須放到通知裡,如文中所示。

3、中間方塊是通過CALayer兩步實現的,第一步設定整個背景顏色,這個顏色根據中間想顯示的樣式來設定;第二步在代理方法裡面重新設定一次背景顏色,這個顏色根據除中間以外的區域來設定,然後將中間的挖掉。但是必須呼叫setNeedsDisplay方法,否則代理方法不會呼叫。

二、參考文獻

1、iOS開發系列--音訊播放、錄音、視訊播放、拍照、視訊錄製
2、iOS開發 - 二維碼的掃描
3、iOS二維碼掃描與生成(優化啟動卡頓)

三、原始碼

相關文章