当另一个视图从AVCaptureDevice API获取新值时,SwiftUI Picker视图的奇怪行为
我不熟悉SwiftUI和Combine。我尝试构建的是一个手动摄像头应用程序,只有4个UI组件:当另一个视图从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
用于从相机拍摄capture按钮
用于手动控制相机对焦曝光FocusPicker
用于显示曝光级别OffsetView
用于将UIKit摄像头集成到SwiftUI视图中CameraPreviewRepresentable
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)
}
}
}