Search code examples
swiftuiuikitcombinefoundation

SwiftUI doesn't update state to @ObservedObject cameraViewModel object


I'm new to SwiftUI and manual camera functionality, and I really need help.

So I trying to build a SwiftUI camera view that has a UIKit camera as a wrapper to control the focus lens position via SwiftUI picker view, display below a fucus value, and want to try have a correlation between AVcaptureDevice.lensPosition from 0 to 1.0 and feats that are displayed in the focus picker view. But for now, I only want to display that fucus number on screen.

And the problem is when I try to update focus via coordinator focus observation and set it to the camera view model then nothing happened. Please help 🙌

Here's the code:

import SwiftUI
import AVFoundation
import Combine

struct ContentView: View {
    
    @State private var didTapCapture = false
    @State private var focusLensPosition: Float = 0
    @ObservedObject var cameraViewModel = CameraViewModel(focusLensPosition: 0)
    
    var body: some View {
        
        VStack {
            ZStack {
                CameraPreviewRepresentable(didTapCapture: $didTapCapture, cameraViewModel: cameraViewModel)
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
                
                VStack {
                    FocusPicker(selectedFocus: $focusLensPosition)
                    
                    Text(String(cameraViewModel.focusLensPosition))
                        .foregroundColor(.red)
                        .font(.largeTitle)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
            .edgesIgnoringSafeArea(.all)
            
            Spacer()
            
            CaptureButton(didTapCapture: $didTapCapture)
                .frame(width: 100, height: 100, alignment: .center)
                .padding(.bottom, 20)
        }
        
        
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

struct CaptureButton: View {
    @Binding var didTapCapture : Bool
    
    var body: some View {
        Button {
            didTapCapture.toggle()
            
        } label: {
            Image(systemName: "photo")
                .font(.largeTitle)
                .padding(30)
                .background(Color.red)
                .foregroundColor(.white)
                .clipShape(Circle())
                .overlay(
                    Circle()
                        .stroke(Color.red)
                )
        }
    }
}

struct CameraPreviewRepresentable: UIViewControllerRepresentable {
    
    @Environment(\.presentationMode) var presentationMode
    @Binding var didTapCapture: Bool
    @ObservedObject var cameraViewModel: CameraViewModel
    
    let cameraController: CustomCameraController = CustomCameraController()
    
    func makeUIViewController(context: Context) -> CustomCameraController {
        cameraController.delegate = context.coordinator
        
        return cameraController
    }
    
    func updateUIViewController(_ cameraViewController: CustomCameraController, context: Context) {
        
        if (self.didTapCapture) {
            cameraViewController.didTapRecord()
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self, cameraViewModel: cameraViewModel)
    }
    
    class Coordinator: NSObject, UINavigationControllerDelegate, AVCapturePhotoCaptureDelegate {
        let parent: CameraPreviewRepresentable
        var cameraViewModel: CameraViewModel
        
        var focusLensPositionObserver: NSKeyValueObservation?
        
        init(_ parent: CameraPreviewRepresentable, cameraViewModel: CameraViewModel) {
            self.parent = parent
            self.cameraViewModel = cameraViewModel
            super.init()
            
            focusLensPositionObserver = self.parent.cameraController.currentCamera?.observe(\.lensPosition, options: [.new]) { [weak self] camera, _ in

                print(Float(camera.lensPosition))
                
                //announcing changes via Publisher
                self?.cameraViewModel.focusLensPosition = camera.lensPosition
            }
        }
        
        deinit {
            focusLensPositionObserver = nil
        }
        
        func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
            
            parent.didTapCapture = false
            
            if let imageData = photo.fileDataRepresentation(), let image = UIImage(data: imageData) {
                UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
            }
            
            parent.presentationMode.wrappedValue.dismiss()
        }
    }
}

class CameraViewModel: ObservableObject {
    @Published var focusLensPosition: Float = 0

    init(focusLensPosition: Float) {
        self.focusLensPosition = focusLensPosition
    }
}

class CustomCameraController: UIViewController {
    
    var image: UIImage?
    
    var captureSession = AVCaptureSession()
    var backCamera: AVCaptureDevice?
    var frontCamera: AVCaptureDevice?
    var currentCamera: AVCaptureDevice?
    var photoOutput: AVCapturePhotoOutput?
    var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
    
    //DELEGATE
    var delegate: AVCapturePhotoCaptureDelegate?
    
    func showFocusLensPosition() -> Float {
//        guard let camera = currentCamera else { return 0 }
        
//        try! currentCamera!.lockForConfiguration()
//        currentCamera!.focusMode = .autoFocus
////        currentCamera!.setFocusModeLocked(lensPosition: currentCamera!.lensPosition, completionHandler: nil)
//        currentCamera!.unlockForConfiguration()
        
        return currentCamera!.lensPosition
    }
    
    func didTapRecord() {
        
        let settings = AVCapturePhotoSettings()
        photoOutput?.capturePhoto(with: settings, delegate: delegate!)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }
    
    func setup() {
        
        setupCaptureSession()
        setupDevice()
        setupInputOutput()
        setupPreviewLayer()
        startRunningCaptureSession()
    }
    
    func setupCaptureSession() {
        captureSession.sessionPreset = .photo
    }
    
    func setupDevice() {
        let deviceDiscoverySession =
            AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera],
                                                                      mediaType: .video,
                                                                      position: .unspecified)
        for device in deviceDiscoverySession.devices {
            
            switch device.position {
            case .front:
                self.frontCamera = device
            case .back:
                self.backCamera = device
            default:
                break
            }
        }
        
        self.currentCamera = self.backCamera
    }
    
    func setupInputOutput() {
        do {
            
            let captureDeviceInput = try AVCaptureDeviceInput(device: currentCamera!)
            captureSession.addInput(captureDeviceInput)
            photoOutput = AVCapturePhotoOutput()
            captureSession.addOutput(photoOutput!)
            
        } catch {
            print(error)
        }
        
    }
    
    func setupPreviewLayer() {
        
        self.cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        self.cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
        
        let deviceOrientation = UIDevice.current.orientation
        cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation(rawValue: deviceOrientation.rawValue)!
        
        self.cameraPreviewLayer?.frame = self.view.frame
//        view.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
        self.view.layer.insertSublayer(cameraPreviewLayer!, at: 0)
    }
    
    func startRunningCaptureSession() {
        captureSession.startRunning()
    }
}


struct FocusPicker: View {
    
    var feets = ["∞ ft", "30", "15", "10", "7", "5", "4", "3.5", "3", "2.5", "2", "1.5", "1", "0.5", "Auto"]
    
    @Binding var selectedFocus: Float
    
    var body: some View {
        
        Picker(selection: $selectedFocus, label: Text("")) {
            ForEach(0 ..< feets.count) {
                Text(feets[$0])
                    .foregroundColor(.white)
                    .font(.subheadline)
                    .fontWeight(.medium)
                
            }
            .animation(.none)
            .background(Color.clear)
            .pickerStyle(WheelPickerStyle())
        }
        .frame(width: 60, height: 200)
        .border(Color.gray, width: 5)
        .clipped()
    }
}

Solution

  • The problem with your provided code is that the type of selectedFocus within the FocusPicker view should be Integer rather than Float. So one option is to change this type to Integer and find a way to express the AVCaptureDevice.lensPosition as an Integer with the given range.

    The second option is to replace the feets array with an enumeration. By making the enumeration conform to the CustomStringConvertible protocol, you can even provide a proper description. Please see my example below.

    I've stripped your code a bit as you just wanted to display the number in the first step and thus the code is more comprehensible.

    My working example:

    import SwiftUI
    import Combine
    
    struct ContentView: View {
        @ObservedObject var cameraViewModel = CameraViewModel(focusLensPosition: 0.5)
        
        var body: some View {
            VStack {
                ZStack {
                    VStack {
                        FocusPicker(selectedFocus: $cameraViewModel.focusLensPosition)
                        
                        Text(String(self.cameraViewModel.focusLensPosition))
                            .foregroundColor(.red)
                            .font(.largeTitle)
                    }
                    .frame(maxWidth: .infinity, alignment: .leading)
                }
                .edgesIgnoringSafeArea(.all)
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
    class CameraViewModel: ObservableObject {
        @Published var focusLensPosition: Float
    
        init(focusLensPosition: Float) {
            self.focusLensPosition = focusLensPosition
        }
    }
    
    enum Feets: Float, CustomStringConvertible, CaseIterable, Identifiable {
        case case1 = 0.0
        case case2 = 0.5
        case case3 = 1.0
        
        var id: Float { self.rawValue }
        var description: String {
            get {
                switch self {
                case .case1:
                    return "∞ ft"
                case .case2:
                    return "4"
                case .case3:
                    return "Auto"
                }
            }
        }
    }
    
    struct FocusPicker: View {
        @Binding var selectedFocus: Float
        
        var body: some View {
            Picker(selection: $selectedFocus, label: Text("")) {
                ForEach(Feets.allCases) { feet in
                    Text(feet.description)
                }
                .animation(.none)
                .background(Color.clear)
                .pickerStyle(WheelPickerStyle())
            }
            .frame(width: 60, height: 200)
            .border(Color.gray, width: 5)
            .clipped()
        }
    }