【Swift/SwiftUI】AVCaptureを使用してカメラ起動(準備編)

目次

概要

AVCaptureを利用してカメラ起動するのカメラ起動処理、撮影処理に関して記載。
長くなったためこちらで独立したページを作成する。

このページでは以下の項目を記載。

  • カメラ起動
  • 撮影処理
  • 撮影した画像をView側に渡す

AVCaptureを使用してカメラ起動すると、カメラ画面でさまざまなカスタマイズができるようになる。しかし、起動するにしてもやはり内容がわかっていた方が今後対応をする際にもいいと思うため記載。

ソースコード

Info.plist

「Privacy – Camera Usage Description」を追加しないと始まらない。
Valueの部分はカメラにアクセスするかどうかのダイアログに表示される文言となる。

View部分

import SwiftUI

protocol CaptureCameraViewDelegate {
    func passImage(image: UIImage)
}

struct CaptureCameraView: View {
    @Environment(\.presentationMode) var presentation
    
    @ObservedObject private(set) var viewModel = CaptureCameraViewModel()
    
    var delegate: CaptureCameraViewDelegate?
    
    var body: some View {
        ZStack {
            CapturePreview(captureCameraViewModel: viewModel)
            VStack {
                Spacer()
                Button {
                    viewModel.capture()
                } label: {
                    Image(systemName: "circle.inset.filled")
                        .resizable(resizingMode: .stretch)
                        .foregroundColor(.white)
                }
                .frame(width: 60, height: 60)
            }
        }
        .onChange(of: viewModel.imageData) { imageData in
            guard let imageData = imageData else {
                return
            }
            delegate?.passImage(image: imageData)
            presentation.wrappedValue.dismiss()
        }
    }
}

struct CapturePreview: UIViewRepresentable {
    private var captureCameraViewModel: CaptureCameraViewModel
    
    init(captureCameraViewModel: CaptureCameraViewModel) {
        self.captureCameraViewModel = captureCameraViewModel
    }
    
    public func makeUIView(context: Context) -> some UIView {
        
        let cameraView = captureCameraViewModel.previewView
        cameraView.frame = UIScreen.main.bounds
        captureCameraViewModel.launchCamera()
        
        return cameraView
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        
    }
}

処理部分

import AVFoundation
import Foundation
import UIKit

protocol CaptureCameraViewModelDelegate {
    func afterCaptureProcess(image: UIImage)
}

class CaptureCameraViewModel: NSObject, ObservableObject {
    
    @Published private(set) var imageData: UIImage?
    var previewView: UIView = UIView()
    
    private var session: AVCaptureSession?
    private var photoOutput: AVCapturePhotoOutput?
    private var captureSetting: AVCapturePhotoSettings?
    
    func launchCamera() {
        if let session = self.session {
            DispatchQueue.global(qos: .background).async {
                session.startRunning()
            }
            return
        }
        
        guard let device = AVCaptureDevice.default(.builtInDualWideCamera, for: .video, position: .back),
              let deviceInput = try? AVCaptureDeviceInput(device: device) else {
            return
        }
        
        let photoOutput = AVCapturePhotoOutput()
        let session = AVCaptureSession()
        switch UIDevice.current.userInterfaceIdiom {
        case .phone:
            session.sessionPreset = .high
        default:
            session.sessionPreset = .photo
        }
        session.addInput(deviceInput)
        session.addOutput(photoOutput)
        
        let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: session)
        videoPreviewLayer.videoGravity = .resizeAspectFill
        videoPreviewLayer.connection?.videoOrientation = .portrait
        videoPreviewLayer.frame = previewView.bounds
        
        
        DispatchQueue.global(qos: .background).async {
            session.startRunning()
        }
        
        self.photoOutput = photoOutput
        self.session = session
        
        previewView.layer.insertSublayer(videoPreviewLayer, at: 0)
    }
    
    func closeCamera() {
        session?.stopRunning()
    }
    
    func capture() {
        Task { @MainActor in
            let captureSetting = AVCapturePhotoSettings()
            captureSetting.flashMode = .off
            photoOutput?.capturePhoto(with: captureSetting, delegate: self)
            self.captureSetting = captureSetting
        }
    }
}

extension CaptureCameraViewModel: AVCapturePhotoCaptureDelegate {
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if error != nil {
            print("error")
            return
        }
        
        guard let fileData = photo.fileDataRepresentation(),
              let image = UIImage(data: fileData) else {
            return
        }
        print("image")
        imageData = image
    }
}

詳細

画面の立ち上げと同時にカメラの準備をしている。
カメラの準備をしている箇所はlaunchCameraメソッド

まずはこの部分

let device = AVCaptureDevice.default(.builtInDualWideCamera,
                                     for: .video,
                                     position: .back)

この部分はデバイスのどのような撮影モードで行うかを決定する。

AVCaptureDevice.default(公式ドキュメント

引数名内容
deviceType撮影装置の設定(どのようなカメラモードか)
for mediaTypeメディアの設定(ビデオなのか、音声なのか)
positionインカメラかアウトカメラか

deviceTypeには以下のような設定値がある。

設定値内容
builtInUltraWideCameraワイドカメラ(オーソドックスなカメラ)
builtInTelephotoCameraデュアルカメラ(広角・望遠)
builtInDualCamera望遠カメラ
builtInDualWideCameraデプスカメラ(カメラと一緒にデプスセンサーが働く)
builtInTripleCameraトリプルカメラ(超広角・広角・望遠)

for mediaTypeの設定値はたくさんあるが、今回はカメラを使用するため「.video」が好ましい。

positionに関しては今回、何かを撮影することを前提としているため「.back」が好ましい。
「.front」の場合は写真加工アプリや顔認証アプリなどで使われるものだろう。

AVCaptureDeviceInput

続いて、上記の撮影装置の入力をしてセッションに追加する(カメラとの通信を開始する)

let deviceInput = try? AVCaptureDeviceInput(device: device)

ここは、カメラが撮影した画像のデータを受け取るために必要なわけですね!

AVCapturePhotoOutput(公式ドキュメント

このオブジェクトは撮影装置から取得した画像や動画のフレーム処理を行う。

let photoOutput = AVCapturePhotoOutput()

AVCaptureSession(公式ドキュメント

続いてセッションの設定を行う。
まずはAVCaptureSession()のオブジェクトを作成して撮影装置との通信を行うためのオブジェクトを作成する。

let session = AVCaptureSession()

そして、今回必須ではないが、以下のようにsessionPresetを設定することで撮影したデータの解像度などを設定する。

switch UIDevice.current.userInterfaceIdiom {
case .phone:
    session.sessionPreset = .high
default:
    session.sessionPreset = .photo
}

次に、前述したセッションのInputとOutputを追加してアプリとカメラのデータをやりとりできるようにする。

session.addInput(deviceInput)
session.addOutput(photoOutput)

カメラが取得した映像を表示させる

最後に、startRunning()を呼び出してアプリとカメラの通信を開始する。
また、このメソッドを呼び出すときにはbackgroundのスレッドで行う必要があるため、DispatchQueue.global(qos: .background).async内で行う。

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

AVCaptureVideoPreviewLayer(公式ドキュメント

sessionから出力されるデータを表示するレイヤーを作成する。
そして、以下のように細かい設定をしている。

項目内容
videoGravity表示した映像の表示方法(サイズまで引き伸ばすかなど)
videoOrientationカメラの向き
frameレイヤーの大きさ

最後に、sessionから出力されるデータを表示するレイヤーをViewのレイヤーに挿入する。
これでカメラの映像を表示することができる。

let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: session)
videoPreviewLayer.videoGravity = .resizeAspectFill
videoPreviewLayer.connection?.videoOrientation = .portrait
videoPreviewLayer.frame = previewView.bounds

previewView.layer.insertSublayer(videoPreviewLayer, at: 0)

また、カメラの撮影を閉じるときはstopRunning()でカメラとの通信を終了しよう。

func closeCamera() {
    session?.stopRunning()
}

撮影処理(AVCapturePhotoSettingsの公式ドキュメント

撮影処理はcapture()に記載している。
AVCapturePhotoSettingsのオブジェクトで管理していく。
これは、撮影した時の設定を行う。

func capture() {
    Task { @MainActor in
        let captureSetting = AVCapturePhotoSettings()
        captureSetting.flashMode = .off
        photoOutput?.capturePhoto(with: captureSetting, delegate: self)
        self.captureSetting = captureSetting
        session?.stopRunning()
    }
}

ここでフラッシュモードのON/OFFを設定する。

captureSetting.flashMode = .off

そして、以下では撮影した時のデリゲートメソッドを実装する。

photoOutput?.capturePhoto(with: captureSetting, delegate: self)

そうすると、撮影ボタン処理をした際に以下のようなメソッドが呼び出されるようになる。

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    if error != nil {
        print("error")
        return
    }

    guard let fileData = photo.fileDataRepresentation(),
          let image = UIImage(data: fileData) else {
        return
    }
    print("image")
    imageData = image
}

このメソッドは撮影処理が終わったタイミングで呼び出される。
photoに撮影したデータが入っているため、それを画像に変換して変数に格納。
この変数をView部分に渡してあげることでView側で撮影した画像を表示するようにする。

では、View部分を見ていこう。

.onChange(of: viewModel.imageData) { imageData in
    guard let imageData = imageData else {
        return
    }
    delegate?.passImage(image: imageData)
}

先ほどの通り、photoに撮影したデータが入っているため、それを画像に変換して変数すると、変数の値が変わったことで上記の部分の処理が行われる。

そして、撮影前の画面に用意していたデリゲートメソッドを使用して撮影前画面に撮影したデータを渡してあげよう。

参考ページ

Super Hahnah「[Swift] AVFoundation による動画撮影の設定: カメラ種類 / ズーム / 録画時間 / 画質」
zenn「AVFoundationでカメラ種類判定(二眼・三眼など)」