TCA で カメラからの入力をハンドリングする

# 概要
最近 pointfree の The Composable Architecture (TCA) の動画を見ていて、個人開発中のアプリに適用してみたくなりました。その中で、カメラからの入力のハンドリングに少し戸惑ったので、メモとして書きます。

戸惑ったポイント：
- カメラからの入力の取得には delegate を使う必要がある
- カメラからの入力は連続的

解決策：
- カメラからの入力を受け取る delegate メソッドの中で、ある closure に画像情報を流し、その closure は `AsyncStream` に変換する
- `reduce()` 関数の中で `EffectTask.run` を使い、画像が流れてくる `AsyncStream` を `for try await` でハンドリングする

# 実装
## 作る画面

今回は以下のような、カメラからの入力をそのまま表示する単純な画面を作ります。
```swift:ContentView.swift
import SwiftUI

struct ContentView: View {

    var body: some View {
        ZStack {
            if let image = ??? { // 現在のカメラからの入力を受け取る
                GeometryReader { geometry in
                    image
                        .resizable()
                        .scaledToFill()
                        .frame(width: geometry.size.width, height: geometry.size.height)
                }
            } else {
                Color.black
            }
            .edgesIgnoringSafeArea(.all)
            .task {
                viewStore.send(.onTask)
            }
        }
    }
}
```

## ReducerProtocol
最初に、`ReducerProtocol` に準拠した `Camera` を定義します。
```swift:Camera.swift
import ComposableArchitecture
import SwiftUI

public struct Camera: ReducerProtocol {

    public struct State: Equatable {
        var image: Image? // 状態として持つのは画像のみ
    }

    public enum Action: Equatable {
        case onTask // 画面を表示した時に実行したいアクション
        case updateImage(Image?) // State.image を更新するためのアクション
    }

    public func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
        switch action {
        case .onTask:
            return ??? // ここではカメラへのアクセスのチェックとカメラからの入力を受け取って updateImage アクションを send したい

        case let .updateImage(image):
            state.image = image
            return .none
        }
    }
}
```
これを使って、先ほどの View を以下のようにアップデートします。View はこれで完成です。
```swift:ContentView.swift
import ComposableArchitecture
import SwiftUI

struct ContentView: View {

    let store: StoreOf<Camera> // store 追加

    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            ZStack {
                if let image = viewStore.image { // state の 画像を表示する
                    ...
                }
                ...
            }
            .edgesIgnoringSafeArea(.all)
            .task {
                viewStore.send(.onTask) // 画面が表示されたら store に onTask アクションを送る
            }
        }
    }
}
```

## カメラの入力を受け取る

実際にカメラの入力を受け取る actor を追加します。
```swift:LiveVideoStreamClient.swift
actor VideoStreamer {
    var delegate: Delegate?
    let videoOutput = AVCaptureVideoDataOutput()
    let session = AVCaptureSession()
    let sessionQueue = DispatchQueue.global()
}
```
### カメラへのアクセスをチェック

カメラの入力を受け取る前に、まずカメラへのアクセス権限を確認して、もし権限が不明な場合は権限をリクエストします（その時はシステムのアラートが表示されます）
```swift:LiveVideoStreamClient.swift
actor VideoStreamer {
    static func requestPermission() async -> Bool {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .notDetermined:
            return await withCheckedContinuation { continuation in
                AVCaptureDevice.requestAccess(for: .video) { authorized in // カメラへのアクセスをリクエスト
                    continuation.resume(returning: authorized)
                }
            }
        case .authorized:
            return true
        case .restricted, .denied:
            return false
        @unknown default:
            return false
        }
    }
```
### カメラからの入力を取得

まず、カメラからの入力を受け取る session の設定を書きます。
```swift:LiveVideoStreamClient.swift
    private func configureCaptureSession() {
        session.beginConfiguration()
        defer {
            session.commitConfiguration()
        }

        guard
            let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front), // インカメを使います
            let cameraInput = try? AVCaptureDeviceInput(device: device),
            session.canAddInput(cameraInput)
        else { return }

        session.addInput(cameraInput)

        guard session.canAddOutput(videoOutput) else { return }
        session.addOutput(videoOutput)

        let videoConnection = videoOutput.connection(with: .video)
        videoConnection?.videoOrientation = .portrait
        videoConnection?.isVideoMirrored = true // インカメなので反転させます
    }
```

カメラからの入力は `AVCaptureVideoDataOutputSampleBufferDelegate` で受け取れるので、定義します。
受け取った画像を流す必要があるので、closure を定義しておきます。

```swift:LiveVideoStreamClient.swift
// 一部使っている private 関数を省いています
final class Delegate: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {

    let captureOutput: (CIImage?) -> Void // 受け取った画像を流すための closure

    init(captureOutput: @escaping (CIImage?) -> Void) {
        self.captureOutput = captureOutput
    }

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        guard let pixelBuffer = sampleBuffer.imageBuffer else { return }

        if connection.isVideoOrientationSupported,
           let videoOrientation = videoOrientationFor(deviceOrientation) {
            connection.videoOrientation = videoOrientation
        }
        self.captureOutput(CIImage(cvPixelBuffer: pixelBuffer)) // 受け取った画像を CIImage にして流す
    }
}
```
この delegate からカメラ画像を受け取って、AsyncStream で流す関数を書きます。

```swift:LiveVideoStreamClient.swift
actor VideoStreamer {
    func start() -> AsyncStream<Image?> {
        self.configureCaptureSession()

        let stream = AsyncStream<Image?> { continuation in
            self.delegate = Delegate(captureOutput: { ciImage in
                continuation.yield(ciImage.image)
            })
        }
        videoOutput.setSampleBufferDelegate(delegate, queue: DispatchQueue(label: "VideoDataOutputQueue"))

        sessionQueue.async {
            self.session.startRunning()
        }

        return stream
    }
}
```
## Back to ReducerProtocol

画面が表示された時（onTask アクション）に、VideoStreamer のカメラへのアクセス権限をチェックし、アクセスが許可されている時は画像の取得をスタートします。
```swift
public struct Camera: ReducerProtocol {

    let videoStreamer = VideoStreamer() // VideoStreamer 追加（本当は Dependency にする）

    public func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
        switch action {
        case .onTask:
            return .run { send in
                guard await videoStreamer.requestCameraPermission() else { return }
                let stream = await videoStreamer.start()
                for try await image in stream { // AsyncStream で流した画像を for await で処理する
                    await send(.updateImage(image))
                }
            }
        ...
        }
    }
}
```

これでインカメからの画像が表示される画面ができました。

# 参考
- [swift-composable-architecture/Examples/VoiceMemos](https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/VoiceMemos)
- [Previewing the Camera Output](https://developer.apple.com/tutorials/sample-apps/capturingphotos-camerapreview)