11/08/2018, 22:42

Scan QR Code với AVFoundation Framework

I. QR Code là gì QR Code - Quick Response Code là 1 mã vạch (barcode) 2 chiều (2D). Thay vì các đường thẳng đứng như những mã vạch thường thấy trên các sản phẩm thông dụng thì QR Code có các đường thẳng được thiết kế theo cả chiều ngang và chiều dọc. Các dữ liệu được nén vào QR Code sẽ được đọc ...

I. QR Code là gì

QR Code - Quick Response Code là 1 mã vạch (barcode) 2 chiều (2D). Thay vì các đường thẳng đứng như những mã vạch thường thấy trên các sản phẩm thông dụng thì QR Code có các đường thẳng được thiết kế theo cả chiều ngang và chiều dọc. Các dữ liệu được nén vào QR Code sẽ được đọc vởi các hiết bị đặc biệt hoặc các smart phone, tablet sử dụng 1 phần mềm thích hợp. QR Code có thể lưu trữ được 1 lượng lớn dữ liệu so với mã vạch thông thường, nên có thể mã hoá được rất nhiều thông tin trong đó, ví dụ như:

  • URL

  • Phone Number

  • Simple Text hoặc SMS Text.

QR Code được sử dụng chủ yếu vì lý do tiếp thị, nhưng cũng có những tiện ích khác giúp người dùng cảm thấy dễ dàng trong cuộc sống như: in trên danh thiếp để lưu vào danh bạ dễ dàng hơn, scan QR Code để truy cập vào 1 URL đơn giản hơn mà ko phải viết...

alt

II. Chương trình demo:

App khi hoàn thiện sẽ có giao diện như sau:

alt

Giao diện khá đơn giản, từ trên xuống dưới gồm có:

  • 1 UIView và đồng thời cũng là chỗ để người dùng nhìn và scan QR code thông qua việc sử dụng camera của device

  • 1 UILabel (lablPromt Label) chỉ đơn giản là nhắc nhở user chạm vào nút Start! để bắt đầu đọc QR code

  • 1 UIlabel (lblStatus Label) màu xanh để ghi lại kết quả của việc scan và giải mã QR code

  • Cuối cùng là 1 ToolBar và 1 Bar Button Item to start và stop camera để scan

Nguyên tắc hoạt động của ứng dụng như sau:

Ban đầu, Khi app bắt đầu được khởi động, giao diện hiển thị như hình trên. User tap vào button "Start!" và 1 video capturing session sẽ được init để bắt đầu cho việc scan QR code. Ngay khi điều này thực hiện, button "Start!" sẽ chuyển thành "Stop", và chức năng của button cũng sẽ thay đổi để cho ta có thể ngừng chụp bất cứ lúc nào.

Label status sẽ thể hiện 3 message ứng với 3 trạng thái:

  1. Khi code scanning chưa được thực hiện

  2. Khi đang scan code

  3. Khi việc scan đã hoàn tất, thông tin chứa trong mã đã được giải mã và thể hiện trên label này

Ngoài ra 1 hiệu ứng âm thanh được palu mỗi khi 1 QR Code được đọc thành công để app sinh động và tương tác hơn

III. Code:

  1. Cài đặt User Interface:

Danh sách các control cần thiết và các thông số thiết lập như sau (dành cho iPhone - 4 inches):

UIView (view để nhìn và scan QR code) Frame: X=20 Y=40 Width=280 Height=350 Background Color: Black UILabel (add label này vào là subview của view phía trên) Frame: X=17 Y=164 Width=247 Height=21 Color: White Text: Tap on Start! to read a QR Code Font: System Bold 15.0 Text Alignment: Center UILabel (status label) Frame: X=20 Y=448 Width=280 Height=21 Color: R=0 G=255 B=0 Text: QR Code Reader is not yet running… Font: System 15.0 UIToolbar Frame: X=0 Y=524 Width=320 Height=44 UIBarButtonItem (already existing on the toolbar) Title: Start! UIBarButtonItem New Flexible Space Bar Button Item on the left side of the start button. UIBarButtonItem New Flexible Space Bar Button Item on the right side of the start button.

Sau khi hoàn thanh việc thiết lập, giao diện nhìn sẽ giống như giao diện ban đầu như ở trên.

Mở file ViewController.h, và thêm các IBOulet property khai báo như sau:

@interface ViewController : UIViewController

@property (weak, nonatomic) IBOutlet UIView *viewPreview;
@property (weak, nonatomic) IBOutlet UILabel *lblStatus;
@property (weak, nonatomic) IBOutlet UIBarButtonItem *bbitemStart;

@end

Ngoài ra ta cần có sự kiện tap vào button "Start!" nên cần dòng khai báo sau:

[code]

  • (IBAction)startStopReading:(id)sender;

[/code]

Sau đó connect các IBOutlet với các đối tượng trong story board như sau:

alt

  1. Implement QR Code Scan:

Như ta đã biết khi app bắt đầu, button "Start!" sẽ trở thành "Stop", tức là ta cần 1 flag để lưu trạng thái hiện thời. Mở file ViewController.m và bắt đầu thêm flag như sau, đồng thời khi app bắt đầu, ta set giá trị mặc đinh cho flag là false

@interface ViewController ()
@property (nonatomic) BOOL isReading;
@end
- (void)viewDidLoad
{
    [super viewDidLoad];

    _isReading = NO;
}

implent method startStopReading:

- (IBAction)startStopReading:(id)sender {
    if (!_isReading) {
        if ([self startReading]) {
            [_bbitemStart setTitle:@"Stop"];
            [_lblStatus setText:@"Scanning for QR Code..."];
        }
    }
    else{
        [self stopReading];
        [_bbitemStart setTitle:@"Start!"];
    }

    _isReading = !_isReading;
}

Đầu tiên, ta check nếu isReading flag là NO, nghĩa là hiện tại chưa có việc scan QR code, và sẽ gọi method startReading (sẽ implement sau). Nếu method này chạy ko có vấn đề gì, ta sẽ set lại title của button là "Stop" và set lại text của status label. Tuy nhiên, nếu app đang scanning QR code, isReading có giá trị YES, chúng ta gọi method stopReading (cũng sẽ implement sau) và set lại title của button là "Start!". Cuối cùng, ta set lại giá trị của isReading thành giá trị đảo ngược.

Tiếp theo ta cần khai báo các property sau:

@interface ViewController ()
@property (nonatomic) BOOL isReading;

@property (nonatomic, strong) AVCaptureSession *captureSession;
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *videoPreviewLayer;

-(BOOL)startReading;
@end

và đừng quên khai báo header AVFoudation framework bên file .h. đồng thời, bên file.m ta set giá trị mặc định cho captureSession là nil

Method startReading sẽ được implement đầy đủ như sau:

- (BOOL)startReading {
    NSError *error;

    AVCaptureDevice *captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error];
    if (!input) {
        NSLog(@"%@", [error localizedDescription]);
        return NO;
    }

    _captureSession = [[AVCaptureSession alloc] init];
    [_captureSession addInput:input];

    AVCaptureMetadataOutput *captureMetadataOutput = [[AVCaptureMetadataOutput alloc] init];
    [_captureSession addOutput:captureMetadataOutput];

    dispatch_queue_t dispatchQueue;
    dispatchQueue = dispatch_queue_create("myQueue", NULL);
    [captureMetadataOutput setMetadataObjectsDelegate:self queue:dispatchQueue];
    [captureMetadataOutput setMetadataObjectTypes:[NSArray arrayWithObject:AVMetadataObjectTypeQRCode]];

    _videoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession];
    [_videoPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
    [_videoPreviewLayer setFrame:_viewPreview.layer.bounds];
    [_viewPreview.layer addSublayer:_videoPreviewLayer];

    [_captureSession startRunning];

    return YES;
}

Ở đây chúng ta sử dụng class AVCaptureDeviceInput để xác định thiết bị đầu vào (camera). Nếu có lỗi xảy ra, ta kết thúc hàm startReading và trả về NO.

Tiếp theo, 1 capture session được khởi tạo và đòi hỏi cả input và output để hoạt động. Ở đây input được lấy từ camera, còn output chính là dạng đọc được của các metadata được ghi từ thiết bị đầu vào. CHúng ta cần phải thiết lập self làm delegate cho object captureMetadataOutput. đó là lý do ta kế thừa protocol AVCaptureMetadataOutputObjectsDelegate. Thực hiện bằng method "setMetadataObjectsDelegate:queue": với đầu vào là 1 delegate và 1 dispatch queue mà method của delegate sẽ được thực hiện trên đó. Theo các tài liệu chính thức, queue này phải là 1 serial dispatch queue, và không nên thực hiện bất kỳ 1 task nào khác ngoại trù task được assigned. đó là lý do ta sử dụng dispatch_queue_t. Method "setMetadataObjectTypes" cũng khá quan trọng, nó chỉ ra loại metadata mà ta muốn scan, cụ thể ở đây là "AVMetadataObjectTypeQRCode".

Chúng ta đã setup và config 1 đối tượng AVCaptureMetadataOutput, chúng ta cần show cho user thấy camera. Điều này được thực hiện bằng cách sử dụng AVCaptureVideoPreviewlayer, mà thực ra là 1 CALayer, và được add vào layer của view viewPreview như 1 sub layer.

Chỉ có 1 delegate method duy nhất mà AVCaptureMetadataOutputObjectsDelegate cung cấp là "captureOutput:didOutputMetadataObjects:fromConnection:": đây là phần quan trọng thứ 2 của app, bởi vì ở đấy các metadata được nhìn thấy bởi camera được xử lý và giải mã để user có thể đọc được. Delegate method được implement như sau:

-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    if (metadataObjects != nil && [metadataObjects count] > 0) {
        AVMetadataMachineReadableCodeObject *metadataObj = [metadataObjects objectAtIndex:0];
        if ([[metadataObj type] isEqualToString:AVMetadataObjectTypeQRCode]) {
            [_lblStatus performSelectorOnMainThread:@selector(setText:) withObject:[metadataObj stringValue] waitUntilDone:NO];

            [self performSelectorOnMainThread:@selector(stopReading) withObject:nil waitUntilDone:NO];
            [_bbitemStart performSelectorOnMainThread:@selector(setTitle:) withObject:@"Start!" waitUntilDone:NO];
            _isReading = NO;
        }
    }
}

Khi lấy được 1 đối tượng metadata, chúng ta dừng việc đọc, vì thế chúng ta chỉ quan tâm đến đối tượng đầu tiên trong mảng metadataObjects. 1 metadata object được thể hiện bởi 1 class AVMetadataMachineReadableCodeObject và mỗi khi nhận được, chúng ta phải check xem nó có phải là định dạng mong muốn ko. Khi điều kiện là đúng, app chỉ cần đọc mã QR hợp lệ. Đến đây, chúng ta dừng việc đọc QR code lại, đổi lại title của button và show QR code data ra status label. Trước khi show tất cả những điều này, hãy nhớ rằng code của chúng ta hiện vẫn chạy trên secondary thread, vì vậy mọi thứ phải được sử lý ở mainthread để thể hiện ngay lập tức.

method stopReading

-(void)stopReading{
    [_captureSession stopRunning];
    _captureSession = nil;

    [_videoPreviewLayer removeFromSuperlayer];
}

-(void)loadBeepSound{
    NSString *beepFilePath = [[NSBundle mainBundle] pathForResource:@"beep" ofType:@"mp3"];
    NSURL *beepURL = [NSURL URLWithString:beepFilePath];
    NSError *error;

    _audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:beepURL error:&error];
    if (error) {
        NSLog(@"Could not play beep file.");
        NSLog(@"%@", [error localizedDescription]);
    }
    else{
        [_audioPlayer prepareToPlay];
    }
}
0