📑

AVFoundationのカメラをSwiftUIで使用する

2023/08/27に公開

2種類の方法を試した

  • UIViewControllerを自作して UIViewControllerRepresentableして使用する
  • previewViewをUIViewRepresentableして使用する

なお、AVFoundationのカメラセッションの設定は公式Documentationからコピペした。
以下2種類のコードはカメラセッションを開始し、セッション中に得られるプレビュー画面を表示するだけのコードである。写真撮影や動画録画機能は加えていない。

UIViewControllerを自作してUIViewControllerRepresentableして使用する

import AVFoundation
import UIKit
import SwiftUI

// UIViewControllerの定義
class CameraUIViewController: UIViewController {
//    カメラセッションをclassプロパティとして定義
    let captureSession = AVCaptureSession()
    override func viewDidLoad() {
        super.viewDidLoad()
        captureSession.beginConfiguration()
        connectInputsToSession()
        connectOutputToSession()
        captureSession.commitConfiguration()
        let previewView = PreviewView()
        previewView.videoPreviewLayer.session = self.captureSession
        view.addSubview(previewView)
        previewView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            previewView.topAnchor.constraint(equalTo: view.topAnchor),
            previewView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            previewView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            previewView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        DispatchQueue.global(qos: .background).async {
            self.captureSession.startRunning()
        }
    }
//    Input設定
    private func connectInputsToSession() {
        let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .unspecified)
        guard let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice!), captureSession.canAddInput(videoDeviceInput)
        else {
            print("error")
            return
            
        }
        captureSession.addInput(videoDeviceInput)
    }
//    Output設定
    private func connectOutputToSession() {
        let photoOutput = AVCapturePhotoOutput()
        guard captureSession.canAddOutput(photoOutput)
        else {
            print("error")
            return
            
        }
        captureSession.sessionPreset = .photo
        captureSession.addOutput(photoOutput)
    }
//    preview用画面UIView
    class PreviewView: UIView {
        override class var layerClass: AnyClass {
            return AVCaptureVideoPreviewLayer.self
        }
        var videoPreviewLayer: AVCaptureVideoPreviewLayer {
            return layer as! AVCaptureVideoPreviewLayer
        }
    }
}
// CameraUIViewControllerをSwiftUIのViewに変換
struct CameraUIViewControllerRepresentable: UIViewControllerRepresentable {
    typealias UIViewControllerType = CameraUIViewController
    func makeUIViewController(context: Context) ->  UIViewControllerType {
        return CameraUIViewController()
    }
    func updateUIViewController(_ uiViewController:  UIViewControllerType, context: Context) {
        
    }
}
// Canvasでプレビュー(カメラが接続できないのでクラッシュする)
struct CameraUIViewControllerRepresentablePreview: PreviewProvider {
    static var previews: some View {
        CameraUIViewControllerRepresentable()
    }
}

以上で下記部分についてはChatGPTに記述してもらった。この部分がないと実機でリアルタイムのカメラpreview画面が表示されない。

 view.addSubview(previewView)
        previewView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            previewView.topAnchor.constraint(equalTo: view.topAnchor),
            previewView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            previewView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            previewView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])

previewViewをUIViewRepresentableして使用する

import SwiftUI
import AVFoundation
// Viewの定義
struct CameraView: View {
//    カメラセッションをclassプロパティとして定義
    let captureSession = AVCaptureSession()
    var body: some View {
        PreviewViewUIView(captureSession: captureSession)
            .onAppear(perform: {setupCamera()})
    }
    func setupCamera() {
        captureSession.beginConfiguration()
        connectInputsToSession()
        connectOutputToSession()
        captureSession.commitConfiguration()
        DispatchQueue.global(qos: .background).async {
            self.captureSession.startRunning()
        }
    }
//    Input設定
    private func connectInputsToSession() {
        let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .unspecified)
        guard let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice!), captureSession.canAddInput(videoDeviceInput)
        else {
            print("error")
            return
            
        }
        captureSession.addInput(videoDeviceInput)
    }
//    Output設定
    private func connectOutputToSession() {
        let photoOutput = AVCapturePhotoOutput()
        guard captureSession.canAddOutput(photoOutput)
        else {
            print("error")
            return
            
        }
        captureSession.sessionPreset = .photo
        captureSession.addOutput(photoOutput)
    }
//    preview用画面UIView
    class PreviewView: UIView {
        override class var layerClass: AnyClass {
            return AVCaptureVideoPreviewLayer.self
        }
        var videoPreviewLayer: AVCaptureVideoPreviewLayer {
            return layer as! AVCaptureVideoPreviewLayer
        }
    }
//    PreviewViewをUIViewからSwiftUI用Viewに変換
    struct PreviewViewUIView: UIViewRepresentable {
        typealias UIViewControllerType = PreviewView
        let captureSession: AVCaptureSession
        func makeUIView(context: Context) ->  UIViewControllerType {
            let previewView = PreviewView()
            previewView.videoPreviewLayer.session = captureSession
            return previewView
        }
        func updateUIView(_ uiViewController:  UIViewControllerType, context: Context) {
        }
    }
}
// Canvasでプレビュー(カメラが接続できないのでクラッシュする)
struct CameraView_Previews: PreviewProvider {
    static var previews: some View {
        CameraView()
    }
}

共通で行ったこと

公式Documentationの

self.captureSession.startRunning()

の部分は

DispatchQueue.global(qos: .background).async {
    self.captureSession.startRunning()
}

としてメインスレッドを避けた。避けないと紫色の警告が出る。

まとめ

UIViewControllerRepresentableはUIViewControllerを、UIViewRepresentableはUIViewを対象としてSwiftUI用Viewに変換するので変換範囲がかなり異なることがわかる。SwiftUIで学び始めた人はpreviewViewをUIViewRepresentableして使用するのがおすすめ。

Discussion