1. 程式人生 > >iOS開發——系統原生的二維碼掃描

iOS開發——系統原生的二維碼掃描

對於現在的App應用來說,掃描二維碼這個功能是再正常不過的一個功能了,在早期開發這些功能的時候,大家或多或少的都接觸過ZXing和ZBar這類的第三方庫,但從iOS7以後,蘋果就給我們提供了系統原生的API來支援我們掃描獲取二維碼,ZXing和ZBar在使用中或多或少有不盡如人意的地方,再之停止更新很久了,所以今天我們就來聊聊如何用系統原生的方法掃描獲取二維碼。

相機許可權

眾所周知,在使用App掃一掃功能的時候,獲取相機許可權是第一步要做的事情,而編寫程式碼的時候也是一樣,首先我們要判斷使用者是否已經授權能夠訪問相機。相機的授權是一組列舉值

  • 授權列舉值
typedef NS_ENUM(NSInteger
, AVAuthorizationStatus) { AVAuthorizationStatusNotDetermined = 0, //當前還沒有確認是否授權 AVAuthorizationStatusRestricted, AVAuthorizationStatusDenied, AVAuthorizationStatusAuthorized } NS_AVAILABLE_IOS(7_0) __TVOS_PROHIBITED;

而這樣一組列舉值,首先我們要判斷AVAuthorizationStatusNotDetermined是否已經授權,而判斷授權情況的方法就是 

  • 判斷授權方法
AVAuthorizationStatus authorizationStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
  • 完整的授權邏輯
    switch (authorizationStatus) {
        case AVAuthorizationStatusNotDetermined:{
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL
granted) { if (granted) { [self initQRScanView]; [self setupCapture]; }else{ NSLog(@"%@",@"訪問受限"); } }]; break; } case AVAuthorizationStatusRestricted: case AVAuthorizationStatusDenied: { NSLog(@"%@",@"訪問受限"); break; } case AVAuthorizationStatusAuthorized:{ //獲得許可權 break; } default: break; }

上面這段程式碼,就是在完成授權方法之後的一段完整的Switch條件判斷授權的邏輯程式碼,而當你獲得許可權時,可以在裡面寫下你想要進一步執行的方法。

掃碼

掃碼是使用系統原生的AVCaptureSession類來發起的,這個類在官方文件中給出的解釋是AVFundation框架中Capture類的中樞,起到管理協調的作用,而掃碼是一個從攝像頭(input)到 解析出字串(output) 的過程,用AVCaptureSession 來協調。其中是通過 AVCaptureConnection 來連線各個 input 和 output,還可以用它來控制 input 和 output 的 資料流向。

  • 建立掃描程式碼
     dispatch_async(dispatch_get_main_queue(), ^{
             AVCaptureSession * session= [[AVCaptureSession alloc] init];
        AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
        NSError *error;
        AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
        if (deviceInput) {
            [session addInput:deviceInput];


            AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init];
            [metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
            [session addOutput:metadataOutput];

            metadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];

            AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
            previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
            previewLayer.frame = self.view.frame;
            [self.view.layer insertSublayer:previewLayer atIndex:0];

            CGFloat screenHeight = ScreenSize.height;
            CGFloat screenWidth = ScreenSize.width;

            self.scanRect = CGRectMake((screenWidth - TransparentArea([QRScanView width], [QRScanView height]).width) / 2,
                                       (screenHeight - TransparentArea([QRScanView width], [QRScanView height]).height) / 2,
                                       TransparentArea([QRScanView width], [QRScanView height]).width,
                                       TransparentArea([QRScanView width], [QRScanView height]).height);

            __weak typeof(self) weakSelf = self;
            [[NSNotificationCenter defaultCenter] addObserverForName:AVCaptureInputPortFormatDescriptionDidChangeNotification
                object:nil
                queue:[NSOperationQueue currentQueue]
                usingBlock:^(NSNotification * _Nonnull note) {
                [metadataOutput setRectOfInterest:CGRectMake(
                            weakSelf.scanRect.origin.y / screenHeight,
                            weakSelf.scanRect.origin.x / screenWidth,
                            weakSelf.scanRect.size.height / screenHeight,
                            weakSelf.scanRect.size.width / screenWidth)];
                            //如果不設定 整個螢幕都會掃描
                }];
            [session startRunning];
        }else{
            NSLog(@"error = %@",error);
        }
    });
}

中間CGRect的設定,是我想根據現在大多數產品的二維碼掃描規則,定義個一個框掃描,這個我們在後面也會說到。

掃描框

掃碼時 previewLayer 的掃描範圍是整個可視範圍的,但有些需求可能需要指定掃描的區域,雖然我覺得這樣很沒有必要,因為整個螢幕都可以掃又何必指定到某個框呢?但如果真的需要這麼做可以設定 metadataOutput 的 rectOfInterest。

  • 設定二維碼掃描框 這段程式碼已經整合在上面的程式碼中,這裡單獨列出來只是給大家看一下,若是要複製使用的話,這段可不用複製
 CGFloat screenHeight = ScreenSize.height;
            CGFloat screenWidth = ScreenSize.width;

            self.scanRect = CGRectMake((screenWidth - TransparentArea([QRScanView width], [QRScanView height]).width) / 2,
                                       (screenHeight - TransparentArea([QRScanView width], [QRScanView height]).height) / 2,
                                       TransparentArea([QRScanView width], [QRScanView height]).width,
                                       TransparentArea([QRScanView width], [QRScanView height]).height);

            __weak typeof(self) weakSelf = self;
            [[NSNotificationCenter defaultCenter] addObserverForName:AVCaptureInputPortFormatDescriptionDidChangeNotification
                object:nil
                queue:[NSOperationQueue currentQueue]
                usingBlock:^(NSNotification * _Nonnull note) {
                [metadataOutput setRectOfInterest:CGRectMake(
                            weakSelf.scanRect.origin.y / screenHeight,
                            weakSelf.scanRect.origin.x / screenWidth,
                            weakSelf.scanRect.size.height / screenHeight,
                            weakSelf.scanRect.size.width / screenWidth)];
                            //如果不設定 整個螢幕都會掃描
                }];

這個self.scanRect是我先前定義的一個二維碼掃描框的尺寸,而賦值我們在現在已經為他們設定好,現在不管適配什麼機型,都會出現在螢幕的中間。

  • 獲取掃描的值
#pragma mark - AVCaptureMetadataOutputObjectsDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects.firstObject;
    if ([metadataObject.type isEqualToString:AVMetadataObjectTypeQRCode] && !self.isQRCodeCaptured) {
        self.isQRCodeCaptured = YES;
        [self showAlertViewWithMessage:metadataObject.stringValue];
    }
}

獲取掃描的值,必須要實現上面的這個代理方法,中間的自定義方法可以略去,直接看實現的步驟就好。

掃描框的外觀

- (void)drawRect:(CGRect)rect{
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetRGBFillColor(context, 40/255.0, 40/255.0, 40/255.0, .5);
    CGContextFillRect(context, rect);
    NSLog(@"%@", NSStringFromCGSize(TransparentArea([QRScanView width], [QRScanView height])));
    CGRect clearDrawRect = CGRectMake(rect.size.width / 2 - TransparentArea([QRScanView width], [QRScanView height]).width / 2,
                                      rect.size.height / 2 - TransparentArea([QRScanView width], [QRScanView height]).height / 2,
                                      TransparentArea([QRScanView width], [QRScanView height]).width,TransparentArea([QRScanView width], [QRScanView height]).height);

    CGContextClearRect(context, clearDrawRect);
    [self addWhiteRect:context rect:clearDrawRect];
    [self addCornerLineWithContext:context rect:clearDrawRect];
}

- (void)addWhiteRect:(CGContextRef)ctx rect:(CGRect)rect {
    CGContextStrokeRect(ctx, rect);
    CGContextSetRGBStrokeColor(ctx, 1, 1, 1, 1);
    CGContextSetLineWidth(ctx, 0.8);
    CGContextAddRect(ctx, rect);
    CGContextStrokePath(ctx);
}

- (void)addCornerLineWithContext:(CGContextRef)ctx rect:(CGRect)rect{

    //畫四個邊角
    CGContextSetLineWidth(ctx, 2);
    CGContextSetRGBStrokeColor(ctx, 83 /255.0, 239/255.0, 111/255.0, 1);//綠色

    //左上角
    CGPoint poinsTopLeftA[] = {
        CGPointMake(rect.origin.x+0.7, rect.origin.y),
        CGPointMake(rect.origin.x+0.7 , rect.origin.y + 15)
    };
    CGPoint poinsTopLeftB[] = {CGPointMake(rect.origin.x, rect.origin.y +0.7),CGPointMake(rect.origin.x + 15, rect.origin.y+0.7)};
    [self addLine:poinsTopLeftA pointB:poinsTopLeftB ctx:ctx];
    //左下角
    CGPoint poinsBottomLeftA[] = {CGPointMake(rect.origin.x+ 0.7, rect.origin.y + rect.size.height - 15),CGPointMake(rect.origin.x +0.7,rect.origin.y + rect.size.height)};
    CGPoint poinsBottomLeftB[] = {CGPointMake(rect.origin.x , rect.origin.y + rect.size.height - 0.7) ,CGPointMake(rect.origin.x+0.7 +15, rect.origin.y + rect.size.height - 0.7)};
    [self addLine:poinsBottomLeftA pointB:poinsBottomLeftB ctx:ctx];
    //右上角
    CGPoint poinsTopRightA[] = {CGPointMake(rect.origin.x+ rect.size.width - 15, rect.origin.y+0.7),CGPointMake(rect.origin.x + rect.size.width,rect.origin.y +0.7 )};
    CGPoint poinsTopRightB[] = {CGPointMake(rect.origin.x+ rect.size.width-0.7, rect.origin.y),CGPointMake(rect.origin.x + rect.size.width-0.7,rect.origin.y + 15 +0.7 )};
    [self addLine:poinsTopRightA pointB:poinsTopRightB ctx:ctx];

    CGPoint poinsBottomRightA[] = {CGPointMake(rect.origin.x+ rect.size.width -0.7 , rect.origin.y+rect.size.height+ -15),CGPointMake(rect.origin.x-0.7 + rect.size.width,rect.origin.y +rect.size.height )};
    CGPoint poinsBottomRightB[] = {CGPointMake(rect.origin.x+ rect.size.width - 15 , rect.origin.y + rect.size.height-0.7),CGPointMake(rect.origin.x + rect.size.width,rect.origin.y + rect.size.height - 0.7 )};
    [self addLine:poinsBottomRightA pointB:poinsBottomRightB ctx:ctx];
    CGContextStrokePath(ctx);
}

- (void)addLine:(CGPoint[])pointA pointB:(CGPoint[])pointB ctx:(CGContextRef)ctx {
    CGContextAddLines(ctx, pointA, 2);
    CGContextAddLines(ctx, pointB, 2);
}

我們對於掃描框是直接採用了複寫drawRect的方法來繪製的,包括我們常見的四個邊框。

  • 二維碼掃描線的樣式

對於二維碼的掃描線,我給定了四種模式

typedef NS_ENUM(NSInteger, ScanLineMode) {
    ScanLineModeNone, //沒有掃描線
    ScanLineModeDeafult, //預設
    ScanLineModeImge,  //以一個圖為掃描線 類似一根綠色的線上下掃動
    ScanLineModeGrid, //網格狀,類似於支付寶的掃一掃
};

所以在我封裝的類裡,切換不同的模式,可以實現各種二維碼掃描的狀態。程式碼稍後會傳到GitHub上分享。

至此就已經完成了基本的二維碼功能,今天的分享也到這裡了。