+3

Scan QrCode Tutorial

Đôi khi các bạn bắt tay làm tính năng scan QrCode cho ứng dụng của mình. Điều đầu tiên nghĩ tới là tìm tới một thư viện một là nó nhanh hơn, tối ưu hơn ... tuy nhiên bản chất thư viện cũng đa phần xây nên từ các framework của apple phát triển. Trong post này mình sẽ hướng dẫn bạn tự viết cho mình scan QrCode sử dụng framework mà apple cung cấp.

1. Overview Demo

Trong demo này mình tạo một sample đơn thuần scan QrCode mà show nội dung trên alert. Trong sample này mình làm theo hướng định nghĩ ra một vùng để scan tức chỉ tại vùng đó mới scan được Qr code. Tuy nhiên về bản chất bạn hoàn toàn có thể bắt QrCode tại bất kỳ vị trí nào trên camera, mễn sao camera detect được Qr Code. Sample kiểu này bạn có thể tham khảo tại https://www.appcoda.com/barcode-reader-swift/. Trong nội dung tutorial này mình sẽ hướng tới việc xác định vị vùng scan Qr code

2. Dựng giao diện

Như đã đề cập, vì sample tập trung vào việc đọc mã Qr nên mình chỉ cần màn hình như sau: Trên đây mình tạo ra bao gồm 1 View bao phủ toàn bộ View chính. View này định nghĩa vùng hiển thị của camera view. Một imageView để định nghĩa vùng quét. Taị sao lại sử dụng một image, đây là phương án để thích nghi với sự thay đổi của khách hàng. Giả sử trong tương lai họ muốn thay đổi hình ảnh vùng quét thành một kiểu khác như ko phải hình vuông mà là mình tam giác chẳng hạn lúc này bạn chỉ cần thay lại image khác là xong. ok! VIew chỉ đơn giản vậy thôi. Constant xong bạn nhớ kéo outlet vào ViewController nhé!

3. Coding

Như đã đề cập từ đầu, post này mình sẽ code dựa vào framework mà apple cung cấp. Đầu tiên bạn phải import thằng import AVFoundation đây là thằng mà mình sẽ dựa vào nó để scan được QrCode. Thêm một em import AudioToolbox cái này chỉ là làm màu khi scan xong để nó rung lên 1 cái thôi 😃 Ngoài 2 biến outlet của view lúc khởi tạo chúng ta cần thêm 2 biến nữa dùng, phần dưới mình sẽ giải thích sau.

var captureSession: AVCaptureSession?
var videoPreviewLayer: AVCaptureVideoPreviewLayer?

Bây giờ tới phần core nhé.

func setupVideoPreviewLayer() {
        self.view.layoutIfNeeded()
        // tạo một instance của AVCaptureDevice, khởi tạo một device object và cung cấp một video như AVMediaType.video
        let captureDevice = AVCaptureDevice.default(for: AVMediaType.video)
        do {
       // tạo một instance của AVCaptureDeviceInput sử dụng device object đã khai báo ở trên
            let input = try AVCaptureDeviceInput(device: captureDevice!)
            // khởi tạo captureSession, thằng này sẽ mở ra một session để thao tác hoàn toàn trên nó
            captureSession = AVCaptureSession()
             // set input cho session
            captureSession?.addInput(input)
           // khởi tạo một AVCaptureMetadataOutput và set nó là output device cho captureSession đã khai báo ở trên
            let captureMetadataOutput = AVCaptureMetadataOutput()
            captureSession?.addOutput(captureMetadataOutput)
            // set delegate cho output. Lưu ý hãy để nó trên main Queue nhé.
            captureMetadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
            // set nhưng  type mà ta có thể nhận. Phần này nếu mở rộng để quét barcode thì bạn nên định nghĩa các type đó ra một mảng và set vào đây nhé
            captureMetadataOutput.metadataObjectTypes = [AVMetadataObject.ObjectType.qr]
            // Khởi tạo video preview layer, đoạn này chỉ đơn thuần là setup lần cuối :D
            videoPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession!)
            videoPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
            self.cameraView.layer.addSublayer(videoPreviewLayer!)
            
            // layer
            let layerRect = self.cameraView.layer.bounds
            self.videoPreviewLayer?.frame = layerRect
            self.videoPreviewLayer?.position = CGPoint(x: layerRect.midX, y: layerRect.midY)
            // Start video capture.
            if self.captureSession?.isRunning == false {
                self.captureSession?.startRunning()
            }
            view.bringSubview(toFront: self.imageFocus)
            
        } catch {
            return
        }
    }

Tiến tới setup view camera để nó đọc Qr code. Ý nghĩa các dòng code mình đã comment ở trên cụ thể rồi. Tuy nhiên tổng quan phần này, bạn hiểu đơn giản khi mở một camera ta cần phải tạo ra một phiên làm việc session. Trong một phiên làm việc như vậy dĩ nhiên phải có input và output. input là nội dung camera và output là metadata mà nó lấy được. Lưu ý lớn nhất ở đây là khi bạn xử lý output bạn cần phải đẩy hết ra main queue nhé. Hiểu đơn giản là anh camera anh ý đọc rất nhanh, liên tục. vậy nên nếu không đẩy ra main để xử lý kết quả đó luôn rất dễ bị crash app nhé. Sau khi setup xong xuôi vậy là camera đã chay và đã đọc được qr code. giờ ta sẽ setup cho việc nhận và hiển thị output đọc ra nhé

extension ViewController: AVCaptureMetadataOutputObjectsDelegate {
    
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        
        // Get the metadata object.
        let metadataObj = metadataObjects[0] as! AVMetadataMachineReadableCodeObject
        
        if let value = metadataObj.stringValue, metadataObj.type == AVMetadataObject.ObjectType.qr {
            self.stopScanQRcode()
            self.showAlertWith(content: value)
        }
    }
    
}

Trong delegate của nó gọi tới function didOutput metadataObjects thằng này sẽ có khi camera detect được Qr code. Khi nhận được nó mình show nó ra trên một alert thôi

func showAlertWith(content: String) {
        let alertController = UIAlertController(title: "Content Qr Code", message: content, preferredStyle: .alert)
        let actionOk = UIAlertAction(title: "Ok", style: .default) { (alert) in
            self.startScanQRcode()
        }
        alertController.addAction(actionOk)
        self.present(alertController, animated: true, completion: nil)
    }

Tuy nhiên cần phải controll việc start và stop camera khi show alert. (Cái này nếu bạn làm với api rất cần nhé. Kiểu scan xong thì call api loading các kiểu rồi ... )

func startScanQRcode() {
        if self.captureSession?.isRunning == false {
            self.captureSession?.startRunning()
        }
    }
    
    func stopScanQRcode() {
        if self.captureSession?.isRunning == true {
            self.captureSession?.stopRunning()
            AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))
        }
    }

Nếu có handle loading nhớ đẩy nó ra main queue nhé. Nhưng thế này người dùng chưa thể biết được qr code quét thế nào nằm ở đâu. Setup lên vùng hiển thị cho nó và bắt nó chỉ được scan trong vùng này thôi. Add thêm đoạn code sau

@objc func didChangeCaptureInputPortFormatDescription(notification: NSNotification) {
        if let metadataOutput = self.captureSession?.outputs.last as? AVCaptureMetadataOutput,
            let rect = self.videoPreviewLayer?.metadataOutputRectConverted(fromLayerRect: self.imageFocus.frame) {
            metadataOutput.rectOfInterest = rect
        }
    }

Khai báo observer

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // NotificationCenter
        NotificationCenter.default.addObserver(self, selector:#selector(self.didChangeCaptureInputPortFormatDescription(notification:)), name: NSNotification.Name.AVCaptureInputPortFormatDescriptionDidChange, object: nil)
        self.startScanQRcode()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // remove notificationCenter
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVCaptureInputPortFormatDescriptionDidChange, object: nil)
    }

cuối cùng thêm đoạn code sau và run

override func viewDidLoad() {
        super.viewDidLoad()
        setupVideoPreviewLayer()
    }

Tổng Kết

Vậy chỉ với một vài line code đơn giản bạn đã setup xong màn hình scan Qr code mà chẳng phải dùng tới bất kỳ lib nào cả. Còn về vấn đề tốc độ hay hiệu năng bạn cứ yên tâm là anh apple làm rất ngon rồi nhé. Thậm chí nhiều thư viện cũng chỉ sử dụng chính framework này thôi mà. Thậm chí với cách làm này chúng ta hoàn toàn có thể handle rất nhiều thứ trong khi scan muốn custom ra cái gì cũng được. Project https://github.com/tienbm92/ScanQrCodeSample


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí