📸

Locked Camera Capture Extension を試してみた #WWDC24

2024/09/27に公開

はじめに

dely株式会社でクラシルリワードの開発をおこなっているiOSエンジニアのkaikaiです!

この記事では、WWDC2024で発表された新機能である、LockedCameraCaptureを利用して、
サンプルアプリを作成してみました!

この記事では、以下の二点を書かせてもらいました!

  • サンプルアプリの実装手順
  • LockedCaptureが弊社で開発しているクラシルリワードにどう活用できるか?

そもそも、LockedCameraCaptureってなに?

WWDC2024で発表された、iOS18から使用できる新機能の一つで、アプリ内のカメラ機能をデバイスがロックされている状態からでも利用できるようにするものです。

導入することによるメリット

今まで、アプリ内のカメラにアクセスするためには、一般的に以下の手順が必要でした。

  1. デバイスを起動する
  2. デバイスのロックを解除する
  3. アプリを起動する
  4. アプリ内で、カメラ機能を立ち上げる
  5. カメラが立ち上がる。

しかし、LockedCameraCaptureを導入することで、カメラ機能までのアクセスを短縮し、ユーザーは瞬時にカメラを起動し、撮影することができます。
例として、以下のような手順に変わります。

  1. デバイスを起動する。
  2. ロック画面に配置したボタンを押下する。(※後述)
  3. カメラが立ち上がる。

カメラが立ち上がるまでの手順がとても少なくなっているのがわかります。

写真を撮影することがコア機能のアプリにとっては、是非とも取り入れたい機能なのではないでしょうか?🤓

実際にやってみる!

今回は、LockedCameraCaptureで提供されている一部機能を使用して、以下の要件の簡単なサンプルアプリを作成してみました!

  • ロック画面から、アプリ内のカメラを起動できる
  • 撮影後、ロック画面からアプリを開く
  • 撮影した写真が、アプリにて確認できる

実装環境

  • Xcode:16.0
  • iOS 18.1
  • 端末:iPhone13

実装手順

カスタムコントロール配置〜カメラ起動まで

①カスタムコントロールを作成する

ロック画面からアプリ内カメラを起動するためには、まずは、アプリと紐づいたカスタムコントロールを作成する必要があります。
そのために、まずはカスタムコントロールを作成します。

まず、File → New → Target から、WidgetExtensionを導入します。

作成する際は、Include Controlにチェックをつけた状態で作成します。
こうすることで、Target追加後の自動生成ファイルに、カスタムコントロールのファイルが自動で含まれるようになります。

Targetを追加したら、以下のファイル群が自動で追加されます。
WidgetExtensionControlが、自動生成されたカスタムコントロールのファイルになります。

この時点で、すでにロック画面に配置できるカスタムコントロールが作られています。
実際にビルドして確かめると、以下のように配置することができます。


ロック画面編集画面に、サンプルアプリのカスタムコントロールが追加


ロック画面にサンプルアプリのカスタムコントロールが配置されている

しかし、現状の状態だと、配置ができるだけで、カスタムコントロールをタップしても、何も発生することはありません。

カスタムコントロールをタップした後のアクションを設定するためには、AppIntentに準拠した構造体を作成し、それをカスタムコントロールにセットする必要があります。
後続で、LockedCameraCaptureを使用するための構造体を作成します。

(自動生成ファイルでは、サンプルとしてStartTimerIntentという構造体がセットされています。)

②CameraCaptureIntentに準拠した構造体を作成する

先ほど作成したカスタムコントロールにセットするための、AppIntentに準拠した構造体を作成します。

WidgetExtension配下に、以下のファイルを作成します。

CaptureIntent.swift
import LockedCameraCapture
import AppIntents

struct CaptureIntent: CameraCaptureIntent {
    static var title: LocalizedStringResource = "CaptureIntent"
    
    @MainActor
    func perform() async throws -> some IntentResult {
        return .result()
    }
}

③CaptureExtensionを導入する

次に、File → New → Targetから、CaptureExtensionを追加します。

以下のようなファイル群が追加されたら、okです。

④作成したCaptureIntentのTarget Membershipを追加する

前述で作成した、CaptureIntentは、App、WidgetExtension、CaptureExtension、それぞれの機能を繋げるような役割を持ちます。
よって、CaptureIntentのTargetMembershipを、以下のように修正します。

⑤カスタムコントロールに、CaptureIntentをセットする

カスタムコントロールを押下した際に、サンプルでセットされているStartTimerIntentではなく、作成したCaptureIntentを呼び出すように修正します。

ついでに、現状のカスタムコントロールのままだと、見た目がカメラが起動するっぽく見えないので、カメラのような見た目に修正しましょう!

以下がコード例になります。

WidgetExtensionControl.swift(変更後)
struct WidgetExtensionControl: ControlWidget {
    var body: some ControlWidgetConfiguration {
        StaticControlConfiguration(kind: コントロール識別子) {  // 変更しなくて良い
            ControlWidgetButton(action: CaptureIntent()) { // CaptureIntentを設定
                Label("LockedCapture", systemImage: "camera.viewfinder") // コントロールのアイコンを変更
            }
        }
        .displayName("カメラを起動")
    }
}

アイコン変更前(before)

アイコン変更後(after)

⑥Info.Plistに修正を加える。

カメラを使用することになる関係上、カメラの使用理由を記述する必要があります。
以下の二つのTargetのinfo.plistに、
Privacy - Camera Usage Description
を追加しましょう。

LockedCameraCaptureSample(App)

CaptureExtension

⑦カメラ利用リクエストの許諾ダイアログを実装する

アプリ本体側で、カメラの利用許諾を前もって取得しておかないと、ロック画面からアプリのカメラを利用することはできません。
よって、アプリを開いたら、カメラ利用許諾をリクエストするように実装します。

ContentView.swift
struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .onAppear {
            Task {
                let _ = await AVCaptureDevice.requestAccess(for: .video)  // カメラ利用許諾をリクエストするコードを追加
            }
        }
        .padding()
    }
}

カメラが起動されるかどうか確認!

ここまできたら、ロック画面にセットしたカスタムコントロールを押下すると、カメラ画面が表示されるはずです!
以下のような挙動になればOKです。

しかし、現状では、まだカメラが起動するのみで、撮影した写真をアプリ本体側へ受け渡し、処理をする実装ができていません。
次にそちらの実装をおこなっていきます。

撮影した写真をアプリ上で確認する

⑧CaptureExtensionViewFinderの修正

CaptureExtensionViewFinderを、以下のように修正します。
写真撮影後、画像を保存する処理を実装します。

CaptureExtensionViewFinder
import SwiftUI
import UIKit
import LockedCameraCapture

struct CaptureExtensionViewFinder: UIViewControllerRepresentable {
    let session: LockedCameraCaptureSession
    var sourceType: UIImagePickerController.SourceType = .camera
    private let delegate: ImagePickerDelegate
    
    init(session: LockedCameraCaptureSession) {
        self.session = session
        self.delegate = .init(session: session)
    }
    
    func makeUIViewController(context: Self.Context) -> UIImagePickerController {
        let imagePicker = UIImagePickerController()
        imagePicker.sourceType = sourceType
        imagePicker.cameraDevice = .rear
        imagePicker.delegate = self.delegate
        
        return imagePicker
    }
    
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Self.Context) {
    }
}

private class ImagePickerDelegate: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    let session: LockedCameraCaptureSession
    
    init(session: LockedCameraCaptureSession) {
        self.session = session
    }
    
    @MainActor
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        guard let pickedImage = info[.originalImage] as? UIImage else { return }
        do {
            let fileURL = self.session.sessionContentURL.appendingPathComponent("LockedCapture")
            try pickedImage.jpegData(compressionQuality: 0.7)?.write(to: fileURL)
            
            Task {
                let activity = NSUserActivity(activityType: NSUserActivityTypeLockedCameraCapture)
                try await self.session.openApplication(for: activity)
            }
        } catch {
            debugPrint(error)
        }
    }
}

⑨保存した画像を、取得する

ContentView内にて、撮影した写真を取得し、画面上に表示したいので、まずは画像自体を取得するメソッドを作成します。

private func getLockedCaptureImage() {
    for url in LockedCameraCaptureManager.shared.sessionContentURLs {
        let fileUrl = url.appendingPathComponent("LockedCapture")
        do {
            let data = try Data(contentsOf: fileUrl)
            guard let uiImage = UIImage(data: data) else { return }
            imageValues.append(uiImage)
        } catch {
            // error handling
        }
    }
}

LockedCameraCaptureManagerのsessionContentURLsで、CaputreExtensionで撮影したデータが保存された先のディレクトリパスを取得することができます。

ディレクトリパスから画像データを取得した後、画像を後述するimageValuesに格納していくことで、アプリ上でも表示できるようにします。

⑩画像を表示する

最後に、取得した画像を表示するためのViewを作成します。

ContentView
struct ContentView: View {
    
    @State var imageValues: [UIImage] = []
    
    var body: some View {
        VStack {
            Button("LockedCaptureで撮影した写真を表示する") {
                getLockedCaptureImage()
            }
            
            ScrollView {
                VStack(spacing: 10) {
                    ForEach(imageValues.indices, id: \.self) { index in
                        Image(uiImage: imageValues[index])
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(maxWidth: .infinity)
                            .padding()
                    }
                }
            }
        }
        .onAppear {
            Task {
                let _ = await AVCaptureDevice.requestAccess(for: .video)
            }
        }
        .padding()
    }
private func getLockedCaptureImage { ~ }
}

動作確認

ここまできたら、実装は完了です!
動作確認すると、以下の挙動を確認することができます。

  1. ロック画面からカメラViewを開く
  2. カメラView内にて、写真を撮影後、UsePhotoからアプリが開く
  3. 開いたアプリ内で、写真を表示するボタン押下で撮影写真表示

これで、ロック状態で撮影した画像を、アプリ内で取り扱うことができました!

また、今回のサンプルアプリは、以下のリポジトリに格納しているので、
実装の際のご参考になれば幸いです!

https://github.com/kaikai8812/LockedCameraCaptureExtension

他にもできそうなこと

今回は、ロック画面にて撮影した写真を、アプリ画面上で表示させる、という要件を満たすためにミニマムな実装を行いました。

これに加えて、今回は実装していませんが、以下のようなことを実現できそうです。

  • アプリの状況によって、カメラViewの状態を変化させる
    • 例えば、撮影用の特別なフィルターを購入したユーザーには、特別なカメラViewを提供するなど
    • 参考資料
  • カメラViewで何かを選択し、そこからのアプリ起動後の状況を変化させる
    • 例えば、SNSアプリの場合、カメラView上で投稿するカテゴリを選択し、アプリ起動後はすでにカテゴリが選択されている状態で投稿できる、など
    • 参考資料

クラシルリワードアプリで導入できそうなこと

弊社で開発しているクラシルリワードアプリでも、カメラを使用している機能が存在します!
レシートを撮影することで、ポイントを取得できるレシチャレというサービスです。

レシチャレの画面

現状、レシチャレへ投稿するレシートを撮影するためには、
複数回のタップを行い、ようやく到達することができます。

LockedCaptureを導入し、アプリをロックしている状態でもレシートを撮影し、投稿することができるので、さらにポイントを貯めやすいアプリになるな!と感じています。

※まだ導入は決まっていませんが、ぜひ今後取り入れていきたい機能なので、提案していく予定です!!💪

感想

今回は、改めてWWDC2024で公開された最新機能であるLockedCaptureを簡単なサンプルアプリで触ってみました。

beta版の時点から、色々と検証していたのですが、最新技術であるがゆえに、以下の課題があり、なかなか技術検証を進めるのが難しかったです。

  • 参考文献が少ない
  • 検証時点で、LockedCaptureを使用しているようなリポジトリがほとんど存在しなかった
  • 不具合が発生した際に、自分の実装が間違っているのか、Xcode上の不具合なのかの判別が難しい
    • LockedCaptureが動かない時がある、とXcodeのRelease Noteに記述があるものの、詳細がわからないなど

ただ、この経験を通じて、1次情報を見にいく大切さを改めて痛感しました。

また、LockedCaptureを実装するためには、LockedCaptureExtensionだけではなく、カスタムコントロールの作成、AppIntentの理解など、色々な要素が登場します。

これらの要素に関しても、浅くですが触れることができたことはとても良い経験になリました。
WWDC2024でも、特にAppIntentは熱いトピックなので、活用できそうな方法があればどんどん取り入れていきたいと感じました!

参考文献

LockedCaptureについて

https://developer.apple.com/jp/videos/play/wwdc2024/10204/

https://developer.apple.com/documentation/LockedCameraCapture/Creating-a-camera-experience-for-the-Lock-Screen

カスタムコントロールについて
https://developer.apple.com/jp/videos/play/wwdc2024/10157/

dely Tech Blog

Discussion