【SwiftUI】カメラのフロントとバックの切り替え

SwiftUIを使ったカメラの実装で、フロントカメラとバックカメラの切り替えについての記事です。

他サイトにもいろんな方法は乗っていると思いますが。自分のメモように残しておこうと思います。

トグルボタンの設定

カメラのアイコンのついたボタンです。これを押すとカメラが切り替わるようにしたいと思います。

Button(action: {
    camera.switchCamera()
}) {
    Image(systemName: "arrow.triangle.2.circlepath.camera")
        .font(.largeTitle)
        .padding()
}
.background(Color.white.opacity(0.7))
.clipShape(Circle())

機能

前回の記事に以下コードを追記すればOKです。

func switchCamera() {
        currentCameraPosition = currentCameraPosition == .front ? .back : .front
        updateCameraPosition()
    }

全コード

特に出し惜しみする必要もないので、ここまでのコードを下に全て載せます。

コピペしてこれが動かなかったら、この記事がもう古くなったことでしょう。

import SwiftUI
import AVFoundation

struct CameraView: View {
    @StateObject var camera = CameraViewModel()

    var body: some View {
        ZStack {
            CameraControll(camera: camera)
                .edgesIgnoringSafeArea(.all)
            VStack {
                Spacer()
                Button(action: {
                    camera.switchCamera()
                }) {
                    Image(systemName: "arrow.triangle.2.circlepath.camera")
                        .font(.largeTitle)
                        .padding()
                }
                .background(Color.white.opacity(0.7))
                .clipShape(Circle())
            }
        }
    }
}

struct CameraControll: UIViewRepresentable {
    @ObservedObject var camera: CameraViewModel

    func makeUIView(context: Context) -> UIView { camera }
    func updateUIView(_ uiView: UIViewType, context: Context) {}
}

class CameraViewModel: UIView, ObservableObject {
    var previewLayer: AVCaptureVideoPreviewLayer?
    var currentCameraPosition: AVCaptureDevice.Position = .front
    var session = AVCaptureSession()

    override func layoutSubviews() {
        super.layoutSubviews()
        _ = initCaptureSession
        previewLayer?.frame = bounds
        fixCameraOrientation()
    }

    lazy var initCaptureSession: Void = {
        updateCameraPosition()
    }()

    private func updateCameraPosition() {
        session.inputs.forEach { session.removeInput($0) }

        guard let device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera],
                                                            mediaType: .video,
                                                            position: .unspecified)
                .devices.first(where: { $0.position == currentCameraPosition }),
              let input = try? AVCaptureDeviceInput(device: device) else { return }

        session.addInput(input)

        if previewLayer == nil {
            let previewLayer = AVCaptureVideoPreviewLayer(session: session)
            layer.insertSublayer(previewLayer, at: 0)
            self.previewLayer = previewLayer
        }

        session.startRunning()
    }

    func switchCamera() {
        currentCameraPosition = currentCameraPosition == .front ? .back : .front
        updateCameraPosition()
    }

    private func fixCameraOrientation() {
        let rotation: CGFloat = CGFloat(getDeviceOrientation().rawValue) * 90
        previewLayer?.transform = CATransform3DMakeRotation(rotation / 180 * CGFloat.pi, 0, 0, 1)
    }

    private func getDeviceOrientation() -> Orientation {
        guard let interfaceOrientation = UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.windowScene?.interfaceOrientation else {
            return .right
        }
        switch interfaceOrientation {
        case .landscapeLeft:
            return .left
        case .landscapeRight:
            return .right
        case .portrait:
            return .down
        case .portraitUpsideDown:
            return .up
        default:
            return .right
        }
    }
}

enum Orientation: Int {
    case right = -1, down = 0, left = 1, up = 2
}

#Preview {
    CameraView()
}