SwiftUI中View与UIViewControllerRepresentable同用PresentationMode引发异常
问题产生原因
- 核心触发点是
@Environment(\.presentationMode)会触发View的不必要重绘:SwiftUI中所有声明了该环境变量的视图,都会在环境值变动时重新计算body内容。你同时在父业务View和MediaPickerPhoto中声明了presentationMode,再加上sheet的content闭包会在父View重绘时重复执行,就导致sheetButtons方法被多次调用、ActionSheet重复创建。 - 补充日志里的Photo Picker服务中断报错,是多次重复初始化
UIImagePickerController导致的系统服务连接紊乱,和视图多次重绘直接相关。 - 额外拼写问题:父View里的
presentationMode被误写为presentaionMode,不过该问题不影响核心逻辑。
修复方案
核心思路是去掉MediaPickerPhoto对presentationMode环境变量的依赖,改用Binding控制弹窗显隐,避免触发不必要的父View重绘:
第一步 修改MediaPickerPhoto实现
struct MediaPickerPhoto: UIViewControllerRepresentable { typealias UIViewControllerType = UIImagePickerController // 移除原有的presentationMode声明,改用Binding控制显隐 @Binding var isPresented: Bool let sourceType: UIImagePickerController.SourceType @Binding var showError: Bool let completion: (UIImage?, String?) -> Void func makeUIViewController(context: Context) -> UIImagePickerController { print("MakeUIViewController") let picker = UIImagePickerController() if sourceType == .camera { picker.videoQuality = .typeMedium } picker.sourceType = sourceType picker.delegate = context.coordinator return picker } func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {} func makeCoordinator() -> MediaCoordinatorPhoto { return MediaCoordinatorPhoto(self) } } class MediaCoordinatorPhoto: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { @EnvironmentObject var authSession: AuthSession let db = Firestore.firestore() let parent: MediaPickerPhoto init(_ parent: MediaPickerPhoto) { print("MediaCoordinatorPhoto") self.parent = parent } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { // 直接通过Binding关闭弹窗,不需要依赖环境变量 defer { parent.isPresented = false } guard let type = (info[.mediaType] as? String)?.lowercased() else { return } if type.contains("image"), let uiImage = info[.originalImage] as? UIImage { let image = uiImage.resized(maxSize: CGSize(width: 1280, height: 1280)) ?? uiImage self.parent.completion(image, nil) } else { print("Invalid media type selected") let error = "There was an error updating your user profile picture. Please try again later." parent.completion(nil, error) } } }
第二步 修改父业务View实现
struct CreateListingView: View { // 保留用于关闭当前页面的声明,不会再触发额外重绘 @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> @State var showMediaPickerSheet = false @State var showLibrary = false @State var showMediaErrorAlert = false // 提前计算相机可用性,避免每次重绘都重复调用系统方法 private let isCameraAvailable = UIImagePickerController.isSourceTypeAvailable(.camera) @frozen enum MediaTypeSource { case library } var body: some View { Button(action: presentMediaPicker) { Text("Button") } .actionSheet(isPresented: $showMediaPickerSheet) { // 直接在闭包内构造按钮,避免调用外部方法导致的重复执行 ActionSheet( title: Text("Add Store Picture"), buttons: isCameraAvailable ? [ .default(Text("Choose Photo")) { presentMediaPicker1(.library) }, .cancel { showMediaPickerSheet = false } ] : [ .default(Text("Choose Photo")) { presentMediaPicker1(.library) }, .cancel { showMediaPickerSheet = false } ] ) } .sheet(isPresented: $showLibrary) { // 传入控制显隐的Binding MediaPickerPhoto( isPresented: $showLibrary, sourceType: .photoLibrary, showError: $showMediaErrorAlert ) { image, error in if let error = error { print(error) } else if let image = image { // 处理选中的图片逻辑 } } } } func presentMediaPicker() { print("PresentMediaPicker1Listing") showMediaPickerSheet = true } private func presentMediaPicker1(_ type: MediaTypeSource) { print("PresentMediaPicker2Listing") showMediaPickerSheet = false switch type { case .library: showLibrary = true } } }
内容的提问来源于stack exchange,提问作者jonthornham
相关产品推荐
相关产品推荐

