Blog スタッフブログ

[Swift6]Concurrency対応QR読み込み画面のコードスニペット

Swift

こんにちは、株式会社MIXシステム開発担当のBloomです。

今回はSwift6で利用できるConcurrencyに対応したQR読み込み画面ViewControllerのコードスニペットを紹介します。

import UIKit
import AVFoundation

protocol CaptureQRDelegate: AnyObject {
    func textCaptured(vc: CaptureQRViewController, text: String)
}

@MainActor
class CaptureQRViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
    nonisolated(unsafe) var captureSession: AVCaptureSession? = nil
    nonisolated(unsafe) var capturedText: String? = nil
    nonisolated(unsafe) var previewLayer: AVCaptureVideoPreviewLayer? = nil
    @IBOutlet weak var previewView: UIView!
    
    nonisolated(unsafe) weak var delegate: CaptureQRDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let stat = AVCaptureDevice.authorizationStatus(for: .video)
        switch stat {
        case .authorized:
            self.setupCaptureSession()
            break
        case .denied:
            self.showAlert()
            break
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
                if granted {
                    DispatchQueue.main.async {
                        self.setupCaptureSession()
                    }
                }
                else{
                    // アラートの表示
                    DispatchQueue.main.async {
                        self.showAlert()
                    }
                }
            })
            
            break
        case .restricted:
            break
        @unknown default:
            break
        }
    }
    
    func setupCaptureSession() {
        captureSession = AVCaptureSession()
        let device = AVCaptureDevice.default(for: .video)
        if device == nil {
            
            self.dismiss(animated: true, completion: {
                // 失敗
            })
            return
        }
        
        do {
            let input = try AVCaptureDeviceInput.init(device: device!)
            captureSession?.addInput(input)
        } catch  {
            self.dismiss(animated: true, completion: {
                // 失敗
            })
            return
        }
        
        let output = AVCaptureMetadataOutput.init()
        output.setMetadataObjectsDelegate(self, queue: .main)
        
        captureSession?.addOutput(output)
        
        output.metadataObjectTypes = [.qr, .ean13, .ean8, .upce, .code39, .code93, .code128, .code39Mod43, .aztec, .dataMatrix, .itf14, .pdf417]
        previewLayer = AVCaptureVideoPreviewLayer(session: captureSession!)
        previewLayer!.frame = previewView.bounds
        previewLayer!.videoGravity = .resizeAspectFill
        if let orientation = self.convertUIOrientation2VideoOrientation(f: { return self.appOrientation() } ) {
            previewLayer!.connection?.videoOrientation = orientation
        }

        previewView.layer.addSublayer(previewLayer!)
        DispatchQueue.global().async {
            self.captureSession?.startRunning()
        }
    }
    func showAlert() {
        DispatchQueue.main.async {
            let alert = UIAlertController.init(title: "カメラの認証が必要です", message: nil, preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { (sender:UIAlertAction) in
                self.dismiss(animated: true, completion: nil)
            }))
        }
    }
    
    nonisolated func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        
        if self.capturedText != nil {
            return
        }
        
        for data in metadataObjects {
            // 読み込み領域内チェックをしたい場合に追加
            /*
            let trans = self.previewLayer!.transformedMetadataObject(for: data)
            print (trans!.bounds)
            let areaFrame = CGRectMake(0, 0, 400, 400)
            if !areaFrame.contains(trans!.bounds) {
                print (areaFrame, "has not contain", trans!.bounds)
                continue
            }
            */
            
            if (data is AVMetadataMachineReadableCodeObject) {
                let text = (data as! AVMetadataMachineReadableCodeObject).stringValue
                self.capturedText = text
            }
        }
        
        if let capturedText = self.capturedText {
            // なんらかの処理
            print(capturedText)
            DispatchQueue.main.async {
                self.delegate?.textCaptured(vc: self, text: capturedText)
            }
        }
    }
    
    func clearCapturedText() {
        capturedText = nil
    }
    
    // UIDeviceOrientation -> AVCaptureVideoOrientationにConvert
    func convertUIOrientation2VideoOrientation(f: () -> UIDeviceOrientation) -> AVCaptureVideoOrientation? {
        let v = f()
        switch v {
        case UIDeviceOrientation.unknown: return nil
        default:
            return ([
                .portrait: .portrait,
                .portraitUpsideDown: .portraitUpsideDown,
                .landscapeLeft: .landscapeLeft,
                .landscapeRight: .landscapeRight
                ])[v]
        }
    }
    func appOrientation() -> UIDeviceOrientation {
        return UIDevice.current.orientation
    }
}

Swift6からSwift Concurrencyの仕様が追加されており、AVCaptureMetadataOutputObjectsDelegateの実装に追加で記述が必要になっています。ここではViewControllerに@MainActorを指定し、クラス変数にnonisolated(unsafe)を指定、さらにnonisolatedキーワードをmetadataOutput関数へ追加することで対応しています。