【问题标题】:Using @Environment PresentationMode in SwiftUI View and UIViewControllerRepresentable Causing Problems在 SwiftUI 视图和 UIViewControllerRepresentable 中使用 @Environment PresentationMode 导致问题
【发布时间】:2021-11-03 18:53:17
【问题描述】:

我正在开发一个通过 UIViewControllerRepresentable 使用 UIImagePickerController 的 SwiftUI 项目。在 UIViewControllerRepresentable 文件中,我添加以下行,以便我可以在 imagePickerController didFinishPickingMediaWithOptions 方法中关闭 ImagePicker。

@Environment(\.presentationMode) var presentationMode

在我的视图文件中,我还希望能够关闭视图,因此我添加了该行。

@Environment(\.presentationMode) var presentaionMode: Binding<PresentationMode>

然而,当这一行被添加到视图中时,它会导致 ActionSheet 被创建四次。一旦从 ActionSheet 中选择了一个项目,它就会被调用两次。打印语句 ShowSheetButtons 是我识别它被多次调用的方式。如果删除此 @Environment 行,则所有操作都按预期执行。发生这种情况是否有原因,我该如何解决。我希望能够在 View 和 UIViewControllerRpresentable 中使用presentationMode。

查看

struct CreateListingView: View {
    // MARK: ++++++++++++++++++++++++++++++++++++++ Properties ++++++++++++++++++++++++++++++++++++++
    
    // Dismiss the View
    @Environment(\.presentationMode) var presentaionMode: Binding<PresentationMode>
    
    // Media Picker
    @State var showMediaPickerSheet = false
    @State var showLibrary = false
    @State var showMediaErrorAlert = false
    @frozen enum MediaTypeSource {
        case library
    }
    
    // MARK: ++++++++++++++++++++++++++++++++++++++ View ++++++++++++++++++++++++++++++++++++++
    var body: some View {
        
        return
        Button(action: {
            self.presentMediaPicker()
        }, label: {
            Text("Button")
        })
        
            .actionSheet(isPresented: $showMediaPickerSheet, content: {
                ActionSheet(
                    title: Text("Add Store Picture"),
                    buttons: sheetButtons()
                )
            }) // Action Sheet
            .sheet(isPresented: $showLibrary, content: {
                MediaPickerPhoto(sourceType: .photoLibrary, showError: $showMediaErrorAlert) { (image, error) in
                    if error != nil {
                        print(error!)
                    } else {
                        guard let image = image else {
                            return
                        }
                    }
                }
            })
    } // View
    
    
    // MARK: ++++++++++++++++++++++++++++++++++++++ Methods ++++++++++++++++++++++++++++++++++++++
    
    // Media Picker Methods
    func presentMediaPicker() {
        print("PresentMediaPicker1Listing")
        self.showMediaPickerSheet = true
    }
    
    func sheetButtons() -> [Alert.Button] {
        print("ShowSheetButtonsListing")
        
        return UIImagePickerController.isSourceTypeAvailable(.camera) ? [
            .default(Text("Choose Photo")) {
                presentMediaPicker1(.library)
            },
            .cancel {
                showMediaPickerSheet = false
            }
        ] : [
            .default(Text("Choose Photo")) {
                presentMediaPicker1(.library)
            },
            .cancel {
                showMediaPickerSheet = false
            }
        ]
    }
    
    private func presentMediaPicker1(_ type: MediaTypeSource) {
        print("PresentMediaPicker2Listing")
        
        showMediaPickerSheet = false
        switch type {
        case .library:
            showLibrary = true
        }
    }
}

UIViewControllerRepresentable

struct MediaPickerPhoto: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIImagePickerController
    /// Presentation wrapper
    @Environment(\.presentationMode) var presentationMode

    /// Source type to present for
    let sourceType: UIImagePickerController.SourceType

    /// Binding for showing error alerts
    @Binding var showError: Bool

    /// Callback for media selection
    let completion: (UIImage?, String?) -> Void

    // MARK: - Representable

    func makeUIViewController(context: UIViewControllerRepresentableContext<MediaPickerPhoto>) -> 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: UIViewControllerRepresentableContext<MediaPickerPhoto>) {
        // no-op
    }

    // MARK: - Coordinator

    func makeCoordinator() -> MediaCoordinatorPhoto {
        return Coordinator(self)
    }
}

/// Coordinator for  media picker
class MediaCoordinatorPhoto: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    
    // AuthSession
    @EnvironmentObject var authSession: AuthSession
    
    let db = Firestore.firestore()
    
    /// Parent picker
    let parent: MediaPickerPhoto

    // MARK: - Init

    init(_ parent: MediaPickerPhoto) {
        print("MediaCoordinatorPhoto")

        self.parent = parent
    }

    // MARK: - Delegate

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        guard let type = (info[.mediaType] as? String)?.lowercased() else {
            return
        }

        if type.contains("image"), let uiImage = info[.originalImage] as? UIImage {
            // We attempt to resize the image to max os 1280x1280 for perf
            // If it fails, we use the original selection image
            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)
            //parent.showError = true
        }

        parent.presentationMode.wrappedValue.dismiss()
    }
}

编辑

2021-11-03 17:20:13.799510-0700 Global Store Exchange[6900:1991500] [lifecycle] [u

478AD6F6-A9E2-4FE9-96D0-D310B754DD59:m(空)] [com.apple.mobileslideshow.photo-picker(1.0)] 连接插件 使用时中断。 2021-11-03 17:20:13.800173-0700 全球商店交换 [6900:1990765] viewServiceDidTerminateWithError:: 错误 域=_UIViewServiceInterfaceErrorDomain 代码=3“(空)” UserInfo={Message=服务连接中断} 2021-11-03 17:20:13.800383-0700 全球商店交换[6900:1990765] [UI] -[PUPhotoPickerHostViewController viewServiceDidTerminateWithError:] 错误错误 域=_UIViewServiceInterfaceErrorDomain 代码=3“(空)” UserInfo={Message=服务连接中断} 2021-11-03 17:20:13.800987-0700 全球商店交换 [6900:1990765] viewServiceDidTerminateWithError:: 错误 域=_UIViewServiceInterfaceErrorDomain 代码=3“(空)” UserInfo={Message=服务连接中断} 2021-11-03 17:20:13.801016-0700 全球商店交换[6900:1990765] [UI] -[PUPhotoPickerHostViewController viewServiceDidTerminateWithError:] 错误错误 域=_UIViewServiceInterfaceErrorDomain 代码=3“(空)” UserInfo={Message=服务连接中断} 2021-11-03 17:20:13.801089-0700 全球商店交换 [6900:1990765] UIImagePickerController UIViewController 创建 错误:错误域=NSCocoaErrorDomain 代码=4097“连接到 名为 com.apple.mobileslideshow.photo-picker.viewservice 的服务” UserInfo={NSDebugDescription=连接到服务命名 com.apple.mobileslideshow.photo-picker.viewservice} 2021-11-03 17:20:13.801266-0700 全球商店交换 [6900:1990765] viewServiceDidTerminateWithError:: 错误 域=_UIViewServiceInterfaceErrorDomain 代码=3“(空)” UserInfo={Message=服务连接中断} 2021-11-03 17:20:13.801313-0700 全球商店交换[6900:1990765] [UI] -[PUPhotoPickerHostViewController viewServiceDidTerminateWithError:] 错误错误 域=_UIViewServiceInterfaceErrorDomain 代码=3“(空)” UserInfo={Message=服务连接中断} 2021-11-03 17:20:13.801421-0700 全球商店交换 [6900:1990765] viewServiceDidTerminateWithError:: 错误 域=_UIViewServiceInterfaceErrorDomain 代码=3“(空)” UserInfo={Message=服务连接中断} 2021-11-03 17:20:13.801459-0700 全球商店交换[6900:1990765] [UI] -[PUPhotoPickerHostViewController viewServiceDidTerminateWithError:] 错误错误 域=_UIViewServiceInterfaceErrorDomain 代码=3“(空)” UserInfo={Message=服务连接中断}

【问题讨论】:

  • 我用显示CreateListingView 的简单父视图尝试了您的代码,但无法重现您的问题。你能确定你有一个minimal reproducible example吗?
  • 是的,我刚刚构建了一个新应用并将 CreateListingView 添加到 ContentView 并添加了一个 MediaPickerPhoto 文件。虽然 ShowSheetButtons 在这种情况下只触发两次,但它仍然会导致问题。该问题发生在设备和模拟器上。你可以在这里找到这个简单项目的 repo。 github.com/jonthornham/EnvironmentTest2
  • 好的,我正在运行代码。我确实看到ShowSheetButtons 打印了四次,这意味着ActionSheet 被渲染了多次。但是,我不一定认为这是固有问题——SwiftUI 视图经常被重新创建/重新渲染。在这种情况下,一定是因为 presentationMode 在工作表出现时被重新计算。这会导致不必要的副作用吗?
  • 是的。在我正在进行的项目中,我允许用户上传多张照片作为市场列表的一部分。多次调用后,图像选择器加载空白并崩溃。相机不会发生这种情况。删除环境后,问题就消失了。
  • 啊——有趣。在 GitHub 存储库中,ContentView 中的 presentaionMode: Binding&lt;PresentationMode&gt; 似乎根本没有被使用——但在你的实际应用程序中,它的存在是关键,对吧?有没有办法在示例中表示它,以便我们可以尝试找到解决方法?

标签: swiftui swiftui-environment


【解决方案1】:

我想出了一个解决这个问题的方法。我没有使用 @EnvironmentObject,而是使用 Bool 和 NavigationLink 中的 isActive 属性。

在显示 CreateListingView 的视图中,我向 NavigationView 添加了一个工具栏。在 ToolbarItem 中,我切换了一个连接到 isActive 属性的 @State 变量。

struct MarketplaceView: View {
    @State private var presentCreateListingView = false

    @EnvironmentObject var marketplaceViewModel: MarketplaceViewModel

    var body: some View {
        NavigationView {
                List {
                    ForEach(self.marketplaceViewModel.filteredListingRowViewModels, id: \.id) { listingRowViewModel in
                            NavigationLink(destination: ListingDetailView()) {
                                ListingRowView(listingRowViewModel: listingRowViewModel)
                            }
                    } // ForEach
                } // List
            .navigationTitle("Marketplace")
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    NavigationLink(destination: CreateListingView(presentCreateListingView: self.$presentCreateListingView), isActive: self.$presentCreateListingView, label: {
                        Button(action: {
                            self.presentCreateListingView.toggle()
                        }, label: {
                            Image(systemName: "plus")
                        })
                    }) // NavigationLink
                } // ToolbarItem
            } // Toolbar
        } // NavigationView
        //.navigationViewStyle(StackNavigationViewStyle())
    } // View
}

然后在 CreateListingView 我有一个 @Binding 到 Bool。然后我使用 Button 来切换 Bool。

struct CreateListingView: View {
    @Binding var presentCreateListingView: Bool
    
    var body: some View {
        Button(action: {
            self.presentCreateListingView.toggle()
        }, label: {
            Text("Back")
        })
    } // View
}

我希望这对某人有所帮助。

【讨论】:

    猜你喜欢
    • 2022-07-06
    • 2021-04-22
    • 2021-08-08
    • 1970-01-01
    • 2021-08-16
    • 1970-01-01
    • 1970-01-01
    • 2020-10-30
    • 2020-06-06
    相关资源
    最近更新 更多