当另一个视图从AVCaptureDevice API获取新值时,SwiftUI Picker视图的奇怪行为

当另一个视图从AVCaptureDevice API获取新值时,SwiftUI Picker视图的奇怪行为,swiftui,avfoundation,combine,Swiftui,Avfoundation,Combine,我不熟悉SwiftUI和Combine。我尝试构建的是一个手动摄像头应用程序,只有4个UI组件: capture按钮用于从相机拍摄 FocusPicker用于手动控制相机对焦曝光 OffsetView用于显示曝光级别 CameraPreviewRepresentable用于将UIKit摄像头集成到SwiftUI视图中 还将用户的隐私请求添加到.Info.plist文件中,以允许使用摄像头功能并保存到Apple Photo App 为了更新数据并将其传递到用户界面,我使用cameravewm

我不熟悉SwiftUI和Combine。我尝试构建的是一个手动摄像头应用程序,只有4个UI组件:

  • capture按钮
    用于从相机拍摄
  • FocusPicker
    用于手动控制相机对焦曝光
  • OffsetView
    用于显示曝光级别
  • CameraPreviewRepresentable
    用于将UIKit摄像头集成到SwiftUI视图中
还将用户的隐私请求添加到.Info.plist文件中,以允许使用摄像头功能并保存到Apple Photo App

为了更新数据并将其传递到用户界面,我使用
cameravewmodel
currentCameraSubject
currentCamera
Publisher来显示
AVCaptureDevice
中的新值,并将其设置为
cameravewmodel

我注意到,
FocusPicker
的一个非常有趣的行为/错误,当我开始与它交互并激发一个新的焦点时,它会不断地回到开始的位置,当
OffsetView
每次都获得一个新的值时

但有趣的是,例如当
OffsetView
具有相同的值时,则
FocusPicker
正常工作。我不知道为什么会这样。请帮帮我,帮我修好真让人沮丧

顺便说一下,它只能在真正的设备上工作

以下是所有代码:

import SwiftUI

//@main
//struct StackOverflowCamApp: App {
//    var cameraViewModel = CameraViewModel(focusLensPosition: 0)
//    let cameraController: CustomCameraController = CustomCameraController()
//
//    var body: some Scene {
//        WindowGroup {
//            ContentView(cameraViewModel: cameraViewModel, cameraController: cameraController)
//        }
//    }
//}

struct ContentView: View {
    
    @State private var didTapCapture = false
    @ObservedObject var cameraViewModel: CameraViewModel
    let cameraController: CustomCameraController
    
    var body: some View {
        
        VStack {
            ZStack {
                CameraPreviewRepresentable(didTapCapture: $didTapCapture, cameraViewModel: cameraViewModel, cameraController: cameraController)
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
                
                VStack {
                    FocusPicker(selectedFocus: $cameraViewModel.focusChoice)
                    
                    Text(String(format: "%.2f", cameraViewModel.focusLensPosition))
                        .foregroundColor(.red)
                        .font(.largeTitle)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
            .edgesIgnoringSafeArea(.all)
            
            Spacer()
            
            OffsetView(levelValue: cameraViewModel.exposureTargetOffset, height: 100)
                .frame(maxWidth: .infinity, alignment: .leading)
            
            CaptureButton(didTapCapture: $didTapCapture)
                .frame(width: 100, height: 100, alignment: .center)
                .padding(.bottom, 20)
        }
    }
}

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 OffsetView: View {
    
    var levelValue: Float
    let height: CGFloat
    
    var body: some View {

        ZStack {
            Rectangle()
                .foregroundColor(.red)
                .frame(maxWidth: height / 2, maxHeight: height, alignment: .trailing)

            Rectangle()
                .foregroundColor(.orange)
                .frame(maxWidth: height / 2, maxHeight: height / 20, alignment: .trailing)
                .offset(x: 0, y: min(CGFloat(-levelValue) * height / 2, height / 2))
        }
    }
}

struct FocusPicker: View {
    
    @Binding var selectedFocus: FocusChoice
    
    var body: some View {
        
        Picker(selection: $selectedFocus, label: Text("")) {
            ForEach(0..<FocusChoice.allCases.count) {
                Text("\(FocusChoice.allCases[$0].caption)")
                    .foregroundColor(.white)
                    .font(.subheadline)
                    .fontWeight(.medium)
                    .tag(FocusChoice.allCases[$0])
            }
            .animation(.none)
            .background(Color.clear)
            .pickerStyle(WheelPickerStyle())
        }
        .frame(width: 60, height: 200)
        .border(Color.gray, width: 5)
        .clipped()
    }
}

import SwiftUI
import Combine
import AVFoundation

struct CameraPreviewRepresentable: UIViewControllerRepresentable {
    
    @Environment(\.presentationMode) var presentationMode
    @Binding var didTapCapture: Bool
    @ObservedObject var cameraViewModel: CameraViewModel
    
    let cameraController: CustomCameraController
    
    func makeUIViewController(context: Context) -> CustomCameraController {
        cameraController.delegate = context.coordinator
        
        return cameraController
    }
    
    func updateUIViewController(_ cameraViewController: CustomCameraController, context: Context) {
        
        if didTapCapture {
            cameraViewController.didTapRecord()
        }
        
        // checking if new value is differnt from the previous value
        if cameraViewModel.focusChoice.rawValue != cameraViewController.manualFocusValue {
            cameraViewController.manualFocusValue = cameraViewModel.focusChoice.rawValue
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self, cameraViewModel: cameraViewModel)
    }
    
    class Coordinator: NSObject, UINavigationControllerDelegate, AVCapturePhotoCaptureDelegate {
        let parent: CameraPreviewRepresentable
        var cameraViewModel: CameraViewModel
        
        var tokens = Set<AnyCancellable>()
        
        init(_ parent: CameraPreviewRepresentable, cameraViewModel: CameraViewModel) {
            self.parent = parent
            self.cameraViewModel = cameraViewModel
            super.init()
            
            // for showing focus lens position
            self.parent.cameraController.currentCamera
                    .filter { $0 != nil }
                    .flatMap { $0!.publisher(for: \.lensPosition) }
                    .assign(to: \.focusLensPosition, on: cameraViewModel)
                    .store(in: &tokens)
            
            // for showing exposure offset
            self.parent.cameraController.currentCamera
                .filter { $0 != nil }
                .flatMap { $0!.publisher(for: \.exposureTargetOffset) }
                .assign(to: \.exposureTargetOffset, on: cameraViewModel)
                .store(in: &tokens)
        }
        
        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()
        }
    }
}

import Combine
import AVFoundation

class CameraViewModel: ObservableObject {
    @Published var focusLensPosition: Float = 0
    @Published var exposureTargetOffset: Float = 0
    
    @Published var focusChoice: FocusChoice = .infinity
    
    private var tokens = Set<AnyCancellable>()

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

enum FocusChoice: Float, CaseIterable {
    case infinity = 1
    case ft_30 = 0.95
    case ft_15 = 0.9
    case ft_10 = 0.85
    case ft_7 = 0.8
    case ft_5 = 0.5
    case ft_4 = 0.7
    case ft_3_5 = 0.65
    case ft_3 = 0.6
    case auto = 0
}

extension FocusChoice {
    var caption: String {
        switch self {
        case .infinity: return "∞ft"
        case .ft_30: return "30"
        case .ft_15: return "15"
        case .ft_10: return "10"
        case .ft_7: return "7"
        case .ft_5: return "5"
        case .ft_4: return "4"
        case .ft_3_5: return "3.5"
        case .ft_3: return "3"
        case .auto: return "Auto"
        }
    }
}

import UIKit
import Combine
import AVFoundation

class CustomCameraController: UIViewController {
    
    var image: UIImage?
    
    var captureSession = AVCaptureSession()
    var backCamera: AVCaptureDevice?
    var frontCamera: AVCaptureDevice?
    lazy var currentCamera: AnyPublisher<AVCaptureDevice?, Never> = currentCameraSubject.eraseToAnyPublisher()
    var photoOutput: AVCapturePhotoOutput?
    var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
    private var currentCameraSubject = CurrentValueSubject<AVCaptureDevice?, Never>(nil)
    
    var manualFocusValue: Float = 1 {
        didSet {
            guard manualFocusValue != 0 else {
                setAutoLensPosition()
                return
            }
            setFocusLensPosition(manualValue: manualFocusValue)
        }
    }
    
    //DELEGATE
    var delegate: AVCapturePhotoCaptureDelegate?
    
    func setFocusLensPosition(manualValue: Float) {
        do {
            try currentCameraSubject.value!.lockForConfiguration()
            currentCameraSubject.value!.focusMode = .locked
            currentCameraSubject.value!.setFocusModeLocked(lensPosition: manualValue, completionHandler: nil)
            currentCameraSubject.value!.unlockForConfiguration()
        } catch let error {
            print(error.localizedDescription)
        }
    }
    
    func setAutoLensPosition() {
        do {
            try currentCameraSubject.value!.lockForConfiguration()
            currentCameraSubject.value!.focusMode = .continuousAutoFocus
            currentCameraSubject.value!.unlockForConfiguration()
        } catch let error {
            print(error.localizedDescription)
        }
    }
    
    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.currentCameraSubject.send(self.backCamera)
    }
    
    func setupInputOutput() {
        do {
          let captureDeviceInput = try AVCaptureDeviceInput(device: currentCameraSubject.value!)
          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
        self.view.layer.insertSublayer(cameraPreviewLayer!, at: 0)
    }
    
    func startRunningCaptureSession() {
        captureSession.startRunning()
    }
}
导入快捷界面
//@主要
//结构StackOverflowCamApp:App{
//var cameraViewModel=cameraViewModel(焦点位置:0)
//让cameraController:CustomCameraController=CustomCameraController()
//
//var body:一些场景{
//窗口组{
//ContentView(cameraViewModel:cameraViewModel,cameraController:cameraController)
//        }
//    }
//}
结构ContentView:View{
@状态私有变量didTapCapture=false
@观测对象变量cameraViewModel:cameraViewModel
让cameraController:自定义cameraController
var body:一些观点{
VStack{
ZStack{
CamerapReviewPresentable(didTapCapture:$didTapCapture,cameraViewModel:cameraViewModel,cameraController:cameraController)
.frame(最大宽度:。无穷大,最大高度:。无穷大,对齐:。中心)
VStack{
FocusPicker(selectedFocus:$cameraViewModel.focusChoice)
文本(字符串(格式:%.2f”,cameraViewModel.focusLensPosition))
.foregroundColor(.red)
.font(.largeTitle)
}
.frame(最大宽度:。无穷大,对齐:。前导)
}
.edgesIgnoringSafeArea(.all)
垫片()
偏移视图(标高值:cameraViewModel.exposureTargetOffset,高度:100)
.frame(最大宽度:。无穷大,对齐:。前导)
CaptureButton(didTapCapture:$didTapCapture)
.框架(宽度:100,高度:100,对齐:。中心)
.padding(.bottom,20)
}
}
}
结构捕获按钮:视图{
@绑定变量:Bool
var body:一些观点{
钮扣{
didTapCapture.toggle()
}标签:{
图像(系统名称:“照片”)
.font(.largeTitle)
.填充(30)
.背景(颜色.红色)
.foregroundColor(.白色)
.clipShape(圆())
.覆盖(
圈()
.笔划(颜色.红色)
)
}
}
}
结构偏移视图:视图{
var levelValue:浮动
让高度:CGFloat
var body:一些观点{
ZStack{
矩形()
.foregroundColor(.red)
.frame(maxWidth:height/2,maxHeight:height,对齐方式:。尾部)
矩形()
.foregroundColor(.橙色)
.frame(maxWidth:height/2,maxHeight:height/20,对齐方式:。尾部)
.偏移量(x:0,y:min(CGFloat(-levelValue)*高度/2,高度/2))
}
}
}
结构FocusPicker:视图{
@绑定变量selectedFocus:FocusChoice
var body:一些观点{
选择器(选择:$selectedFocus,标签:Text(“”)){
ForEach(0..CustomCameraController{
cameraController.delegate=context.coordinator
返回摄影机控制器
}
func updateUIViewController(cameraViewController:CustomCameraController,上下文:上下文){
如果你没有抓到{
cameraViewController.didTapRecord()
}
//检查新值是否与以前的值不同
如果cameraViewModel.focusChoice.rawValue!=cameraViewController.manualFocusValue{
cameraViewController.manualFocusValue=cameraViewModel.focusChoice.rawValue
}
}
func makeCoordinator()->Coordinator{
协调员(自我,cameraViewModel:cameraViewModel)
}
类协调器:NSObject、UINavigationControllerDelegate、AVCapturePhotoCaptureDelegate{
让父项:CameraPreviewRepresentable
var cameraViewModel:cameraViewModel
var tokens=Set()
init(uu父项:CameraPreviewRepresentable,cameraViewModel:cameraViewModel){
self.parent=parent
self.cameraViewModel=cameraViewModel
super.init()
//用于显示聚焦透镜位置
self.parent.cameraController.currentCamera
.filter{$0!=nil}
.flatMap{$0!.publisher(用于:\.lensPosition)}
.分配(到:\.focusLensPosition,打开:cameraViewModel)
.store(在:&to中)
let cameraViewModel: CameraViewModel
FocusPicker(selectedFocus: Binding<FocusChoice>(
    get: {
        cameraViewModel.focusChoice
    },
    set: {
        cameraViewModel.focusChoice = $0
    }
))
struct TextView: View {
    @ObservedObject var cameraViewModel: CameraViewModel
    
    var body: some View {
        Text(String(format: "%.2f", cameraViewModel.focusLensPosition))
            .foregroundColor(.red)
            .font(.largeTitle)
    }
}
struct OffsetView: View {
    @ObservedObject var viewModel : CameraViewModel
    
    let height: CGFloat
    
    var body: some View {

        ZStack {
            Rectangle()
                .foregroundColor(.red)
                .frame(maxWidth: height / 2, maxHeight: height, alignment: .trailing)

            Rectangle()
                .foregroundColor(.orange)
                .frame(maxWidth: height / 2, maxHeight: height / 20, alignment: .trailing)
                .offset(x: 0, y: min(CGFloat(-viewModel.exposureTargetOffset) * height / 2, height / 2))
        }
    }
}
struct ContentView: View {
    
    @State private var didTapCapture = false
    let cameraViewModel: CameraViewModel
    let cameraController: CustomCameraController
    
    var body: some View {
        
        VStack {
            ZStack {
                CameraPreviewRepresentable(didTapCapture: $didTapCapture, cameraViewModel: cameraViewModel, cameraController: cameraController)
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
                
                VStack {
                    FocusPicker(selectedFocus: Binding<FocusChoice>(
                        get: {
                            cameraViewModel.focusChoice
                        },
                        set: {
                            cameraViewModel.focusChoice = $0
                        }
                    ))
                    
                    TextView(cameraViewModel: cameraViewModel)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
            .edgesIgnoringSafeArea(.all)
            
            Spacer()
            
            OffsetView(viewModel: cameraViewModel, height: 100)

                .frame(maxWidth: .infinity, alignment: .leading)
            
            CaptureButton(didTapCapture: $didTapCapture)
                .frame(width: 100, height: 100, alignment: .center)
                .padding(.bottom, 20)
        }
    }
}