📷

iOS アプリにカメラコントロールのオーバーレイの機能を実装する

2024/10/20に公開

カメラコントロールの機能について

CameraControl(Apple Newsroom の画像を参照)

iPhone 16 シリーズからカメラコントロールの機能が追加されました。

あなたが開発した iOS アプリのカメラ機能は、このカメラコントロールに使用することによって、より簡単にカメラ機能を操作することができ、魅力的なアプリとなるでしょう。
(とりあえず、今回はカメラコントロールが操作しずらいという話は置いておく)

カメラ機能を持つアプリに対して、カメラコントロールは大きく分けて 3 つの機能を設定することができます。

  1. カメラコントロールを押してアプリを素早く起動する
  2. アプリ内でカメラコントロールを押すことでシャッターを切る機能
  3. カメラコントロールのオーバーレイを表示しカメラの機能にアクセスする

今回は 2 と 3 について紹介します。

実装の前にカメラコントロールのデザインについて

カメラコントロールの HIG(ヒューマンインターフェイスガイドライン)は日本語翻訳されているようです。

カメラコントロールのデザインについてざっくりとしたまとめ

  • カメラコントロールを軽く押すと、デバイスのベゼルから延びるオーバーレイが表示されるカメラコントロールのオーバーレイ
  • 値の調整ためのスライダとオプションの変更のためのピッカーの 2 つのコントロールが用意されているスライダコントロール/ピッカーコントロール
  • コントロールの機能を表すアイコンは SFSymbol を使用する
  • コントロールのラベルは Dynamic Font を使用するため、視認性が悪くなることを防ぐため、長い名前を避ける(また、縦持ちの場合に画面幅に対して文字の領域が大きくなるため)
  • オーバーレイが表示される場合はアプリ内の UI を最小限に抑える(Apple 謹製のカメラアプリはオーバーレイのコントロールを表示すると、カメラのプレビュー画像とシャッターのみ表示される)
  • カメラの使用状態に応じて有効/無効にする(ビデオ撮影では使用できないものを無効にする)
  • ユーザがカメラコントロールを使用して、どこからでもカメラ撮影を開始できるようにする(こちらに関しては今回実装の紹介は無し)

実装について

カメラコントロールの機能を有効にするためには以下の項目を実装する必要があり、
これらを 1 つでも設定し忘れるとカメラコントロールを押してもオーバーレイが表示されません。

  1. カメラコントロールを強く押した時のカメラ撮影機能
  2. カメラコントロールのオーバーレイの中身
  3. オーバーレイ用の delegate

サンプルプロジェクト

https://github.com/ToshihiroGoto/CameraControl

  • カメラ自体の機能は最低限のものしか実装していません
  • 回転制御を入れ忘れました。なおすのが面倒なので適当に修正してください
  • AVCaptureEventInteraction の振る舞いが変更されたため、サポート OS を iOS 18.1 に変更しています

カメラコントロールを強く押した時のカメラ撮影機能

iOS 17.2 で追加された AVKit の AVCaptureEventInteraction を使用し、アプリ内で音量ボタンを別の機能に変更する設定が必要になります。
「音量ボタン」と「カメラコントロールを強く押す」振る舞いは同じものとなってしまうためこの点は注意してください。

また、カメラコントロールを強く押した際にカメラのシャッターを切る動作以外を設定することはできますが、たぶんアプリの審査で落ちると思われます。

SwiftUI で onCameraCaptureEvent というモディファイアで AVCaptureEventInteraction と同等の機能が追加されたため、こちらに写真撮影のメソッドを追加します。

struct ContentView: View {
    let captureModel: CaptureModel = .init()
    
    var body: some View {
        ZStack {
            GeometryReader { geom in
                CameraPreview(
                    previewFrame:
                        CGRect(
                            x: 0,
                            y: 0,
                            width: geom.size.width,
                            height: geom.size.height
                        ),
                    captureModel: captureModel
                )
                .onCameraCaptureEvent() { event in
                    if event.phase == .began {
                        // 写真撮影するメソッド
                        captureModel.takePhoto()
                    }
                }
            }
   ・
   ・
   ・

iOS 18.0 では onCameraCaptureEvent が正常に動作していなかったため、
以下のように UIViewRepresentable の makeUIView 内で AVCaptureEventInteraction を設定していました。

    let previewFrame: CGRect
    let captureModel: CaptureModel
    
    public func makeUIView(context: Context) -> UICameraPreview {
        let view = UICameraPreview(frame: previewFrame, session: self.captureModel.captureSession)
        view.setupPreview(previewSize: previewFrame)

        let interaction = AVCaptureEventInteraction { event in
            if event.phase == .began {
                // 写真撮影するメソッド
                self.captureModel.takePhoto()
            }
        }
        view.addInteraction(interaction)

        return view
    }

カメラコントロールのオーバーレイの中身

カメラコントロールのオーバーレイの中身はコントロールと呼ばれており、
AVFoundatoin の captureSession の configuration 内で設定します。

以下、設定する手順
1 〜 3 はオーバーレイのコントロールの設定、4 〜 6 はその設定を captureSession に適応します。

  1. カメラコントロールのオーバーレイ用 delegate を設定する
  2. スライダーかピッカーいずれかのコントロールを設定する
  3. コントロールにアクション(振る舞い)を設定する
  4. カメラコントロールが使用可能か調べる
  5. 設定されているコントロールを全て削除する
  6. captureSession にコントロールが設定可能か調べて、可能であればコントロールを設定する

1 カメラコントロールのオーバーレイ用 delegate を設定する

configuration の中でカメラコントロールのオーバーレイ用 delegate を設定します。
また、ここでカメラコントロール用のセッションキューを設定します。
(以降、beginConfiguration / commitConfiguration は割愛)

captureSession.beginConfiguration()
// 中略
let sessionQueue = DispatchSerialQueue(label: "cameraControlOverlay")
captureSession.setControlsDelegate(self, queue: sessionQueue)
// 中略
captureSession.commitConfiguration()

2 スライダかピッカーいずれかのコントロールを設定する

設定できるコントロールはスライダの AVCaptureSlider とピッカーの AVCaptureIndexPicker があり、いずれかを設定し UI と機能を決めます。
コントロールの動作内容は後ほど紹介する setActionQueue で設定します。
AVCaptureSlider / AVCaptureIndexPicker

共通設定

共通の設定として最初の引数にコントロールの内容を表すラベル、2番目の引数にそのアイコンを設定することができます。

アイコンは SFSymbol を使用します。
アイコンにカスタムシンボルを適応することはできますが表示はされません。

また、継承元のクラス AVCaptureControl に isEnabled があり、false でコントロールを使用できないようにすることができます。(デフォルトは true です)
写真撮影では使用できる機能ですが、ビデオ撮影では使用できない場合などコントロールを使えないようにするために isEnabled を使用します。

let captureSlider = AVCaptureSlider("Focus", symbolName: "scope", in: 0...1)
captureSlider.isEnabled = false

AVCaptureSlider

カメラコントロールをスワイプすることで数値を選択できる UI となります。
イニシャライザが 3 つあります。

ClosedRange で 0...1 など数値の幅を決める
let captureSlider = AVCaptureSlider("Focus", symbolName: "scope", in: 0...1)
ClosedRange で数値の幅を決め、step でさらに間隔を決める
let captureSlider = AVCaptureSlider("Focus", symbolName: "scope", in: 0...1, step: 0.2)
配列でスライダに適応する数値を直接設定する
let captureSlider = AVCaptureSlider("Focus", symbolName: "scope", values: [0, 1, 2, 3, 4])

今回は説明を割愛しますが、システムが推奨する範囲内で調整されるコントロールスライダとして、ズーム率を設定する(AVCaptureSystemZoomSlider)と露出補正値を設定する(AVCaptureSystemExposureBiasSlider)の 2 つが用意されています。

AVCaptureIndexPicker

カメラコントロールをスワイプすることで項目を選択できる UI となります。
イニシャライザが 3 つあります。

選択項目にラベルが必要ないもの
let captureIndexPicker = AVCaptureIndexPicker("Focus", symbolName: "scope", numberOfIndexes: 3)
選択項目の配列数を numberOfIndexes で決め、localizedTitleTransform で Int からテキストを返すクロージャーを設定し項目のラベルを定義する
let labels: [String] = ["Label1", "Label2", "Label3"]

let captureIndexPicker = AVCaptureIndexPicker("Focus", symbolName: "scope", numberOfIndexes: 3) { number in
    labels[number]
}
選択項目のラベルをテキストを配列として設定する
let labels: [String] = ["Label1", "Label2", "Label3"]

let captureIndexPicker = AVCaptureIndexPicker("Focus", symbolName: "scope", localizedIndexTitles: labels)

3 コントロールにアクション(振る舞い)を設定する

各コントロールの setActionQueue からコントロールの動作を決めます。

以下のコードでは setFocusModeLocked を使用しスライダに焦点距離(フォーカスの位置)を設定します。
この setActionQueue はデバイスの機能を呼び出していますが、多くの場合はカメラからのキャプチャ画像に対して何らかの加工を行う機能を実装する形となります。

captureSlider.setActionQueue(sessionQueue) { lensPosition in
    do {
        try device.lockForConfiguration()
        device.setFocusModeLocked(lensPosition: lensPosition)
        device.unlockForConfiguration()
    } catch {
        print("焦点距離の変更ができません: \(error)")
    }
}

4 カメラコントロールが使用可能か調べる

captureSession の supportsControls を使い、カメラコントロールが使用可能か調べます。
true で使用可能となります。

if captureSession.supportsControls {
}

5 設定されているコントロールを全て削除する

各コントロールが残ったままとなる状況があるため、captureSession の controls から現在設定している全てのコントロールを列挙し、removeControl で消していきます。

if captureSession.supportsControls {
    for control in captureSession.controls {
        captureSession.removeControl(control)
    }
}

6 captureSession にコントロールが設定可能か調べて、可能であればコントロールを設定する

canAddControl でコントロールが使用可能か調べて、可能であれば addControl で追加します。
カメラコントロールで追加できるコントロールの上限数が決まっており、今回はコードに書いていませんが maxControlsCount でその数を調べることができます。

let controls: [AVCaptureControl] = [captureSlider, captureIndexPicker]
        
if captureSession.supportsControls {
    for control in captureSession.controls {
        captureSession.removeControl(control)
    }
    
    for control in controls {
        if captureSession.canAddControl(control) {
            captureSession.addControl(control)
        } else {
            print("このコントロールは使用できません: \(control)")
        }
    }
}

オーバーレイ用の delegate

各種振る舞いを設定します。
AVCaptureSessionControlsDelegate のプロトコルを使用して、
以下、4つの delegate のメソッドが必要になり、メソッドの中身は空でも動きますが 1 つでも関数が書かれていない場合はオーバーレイが表示されません。

public func sessionControlsDidBecomeActive(_ session: AVCaptureSession) {
    // キャプチャセッションのコントロールがアクティブになり、操作できるようになった時に呼ばれる
}

public func sessionControlsWillEnterFullscreenAppearance(_ session: AVCaptureSession) {
    // キャプチャセッションのコントロールが全画面表示になる時に呼ばれる

    // 主にカメラコントロールのオーバーレイ表示時に
    // 必要のないユーザーインターフェイスを非表示にするために使用する
}

public func sessionControlsWillExitFullscreenAppearance(_ session: AVCaptureSession) {
    // キャプチャセッションのコントロールの全画面表示が終了する時に呼ばれる

    // 主に sessionControlsWillEnterFullscreenAppearance など、
    // 以前に非表示にしたユーザーインターフェイスを元に戻すために使用する
}

public func sessionControlsDidBecomeInactive(_ session: AVCaptureSession) {
    // キャプチャセッションのコントロールが非アクティブになり、操作できなくなった時に呼ばれる
}

カメラコントロールを押してアプリを素早く起動する方法

長くなるため実装方法の詳細は割愛しますが、LockedCameraCapture フレームワークを使用して、デバイスがロックされている時や、他のアプリやホーム画面などでアプリのカメラ機能を起動し、写真や動画撮影を素早く操作するための拡張機能を実装する必要があります。

カメラコントロールから素早くアプリを起動する場合は Capture Extension のみで動きますが、ロック画面から起動する場合は併せて Widget Extension の設定も必要です。

https://developer.apple.com/documentation/LockedCameraCapture
https://developer.apple.com/documentation/lockedcameracapture/creating-a-camera-experience-for-the-lock-screen

参考サイト

https://developer.apple.com/documentation/avfoundation/capture_setup/enhancing_your_app_experience_with_the_camera_control

Discussion