coiai Logo

This article is an English translation of the original Japanese post. Read the Japanese original →

[SwiftUI] Switching Between Front and Back Camera

December 23, 2023

Categories: 未分類

[SwiftUI] Switching Between Front and Back Camera

This post covers implementing a camera in SwiftUI, specifically switching between the front and back camera.

I'm sure other sites cover various approaches to this, but I wanted to keep it here as a note to myself.

Setting up the toggle button

This is a button with a camera icon. Tapping it should switch the camera.

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

Functionality

Just add the following code to the previous post's project and you're good to go.

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

Full code

No reason to hold anything back, so here's the entire code so far.

If you copy and paste this and it doesn't work, this post has probably gone stale by then.

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()
}