You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中View与UIViewControllerRepresentable同用PresentationMode引发异常

问题产生原因

  1. 核心触发点是@Environment(\.presentationMode)会触发View的不必要重绘:SwiftUI中所有声明了该环境变量的视图,都会在环境值变动时重新计算body内容。你同时在父业务View和MediaPickerPhoto中声明了presentationMode,再加上sheet的content闭包会在父View重绘时重复执行,就导致sheetButtons方法被多次调用、ActionSheet重复创建。
  2. 补充日志里的Photo Picker服务中断报错,是多次重复初始化UIImagePickerController导致的系统服务连接紊乱,和视图多次重绘直接相关。
  3. 额外拼写问题:父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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.27 20:36:07