iOS 18中如何在UIKit直接调用SwiftUI的contactAccessPicker(无额外视图层)
问题描述
我在UIKit项目里集成contactAccessPicker组件,这个组件在SwiftUI里是View的扩展,用法如下:
yourView.contactAccessPicker(isPresented: $isPresented) { identifiers in // 获取应用有权限访问的所有联系人 }
但因为它是SwiftUI View的扩展,没法直接在UIKit里调用。我试过用UIHostingController套一层SwiftUI视图来承载它,但会出现多余的视图层级:一是初始的yourView,二是叠加在上面的contactAccessPicker。
我的需求是从底部弹窗里展示这个联系人权限选择器,当前的实现代码如下:
@available(iOS 18.0, *) struct ContactAccessPicker: View { @State var presented = true var handler: ([String]) -> () init(completionHandler: @escaping ([String]) -> ()) { handler = completionHandler } var body: some View { Spacer() .contactAccessPicker(isPresented: $presented, completionHandler: handler) .background(Color.white) } } func presentPicker() { if #available(iOS 18.0, *) { let pickerView = ContactAccessPicker(completionHandler: { [weak self] ids in print(ids) DispatchQueue.main.async { self?.contactPickerViewReference?.view.removeFromSuperview() self?.contactPickerViewReference?.didMove(toParent: nil) self?.contactPickerViewReference?.removeFromParent() } }) let hostingController = UIHostingController(rootView: pickerView) addChild(hostingController) let hostingView = hostingController.view! hostingView.isHidden = true hostingView.translatesAutoresizingMaskIntoConstraints = false hostingView.addSubview(hView) hostingView.pin(to: view) } }
removeFromParent() -> 同时把presenter也移除了
想问问有没有办法直接展示contactAccessPicker不出现额外视图层,或者有没有更高效的UIKit实现方案?
可行方案
1. 让SwiftUI容器视图完全透明
修改ContactAccessPicker的实现,把底层视图改成完全透明的,只保留contactAccessPicker的弹窗逻辑:
@available(iOS 18.0, *) struct ContactAccessPicker: View { @Binding var isPresented: Bool var handler: ([String]) -> () init(isPresented: Binding<Bool>, completionHandler: @escaping ([String]) -> ()) { self._isPresented = isPresented self.handler = completionHandler } var body: some View { Color.clear // 底层用透明颜色,不会显示额外视图 .contactAccessPicker(isPresented: $isPresented, completionHandler: handler) .ignoresSafeArea() // 避免布局受安全区域影响 } }
在UIKit里调用时,不用隐藏hostingView,直接控制弹窗状态:
@available(iOS 18.0, *) func presentContactPicker() { var isPickerPresented = true let pickerView = ContactAccessPicker(isPresented: .constant(isPickerPresented)) { [weak self] ids in print(ids) self?.cleanupContactPicker() } let hostingController = UIHostingController(rootView: pickerView) hostingController.view.backgroundColor = .clear // 容器视图也设为透明 addChild(hostingController) hostingController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(hostingController.view) // 约束铺满整个父视图 NSLayoutConstraint.activate([ hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) hostingController.didMove(toParent: self) contactPickerViewReference = hostingController } @available(iOS 18.0, *) private func cleanupContactPicker() { guard let hostingController = contactPickerViewReference else { return } hostingController.willMove(toParent: nil) hostingController.view.removeFromSuperview() hostingController.removeFromParent() contactPickerViewReference = nil }
这样底层的SwiftUI视图完全看不见,只会显示contactAccessPicker的弹窗,不会有多余的层级。
2. 检查iOS18是否提供UIKit原生API
如果contactAccessPicker是iOS18新增的SwiftUI专属组件,苹果大概率也会提供对应的UIKit接口。你可以查看CNContactPickerViewController的新增属性或方法,或者CNContactStore的权限相关API,直接用原生UIKit实现,这样就不需要嵌套SwiftUI视图了。
3. 修复视图清理逻辑
你当前的清理代码顺序有问题,正确的子控制器移除流程应该先调用willMove(toParent: nil),再移除视图和控制器,这样能避免误删presenter:
DispatchQueue.main.async { guard let controller = self?.contactPickerViewReference else { return } controller.willMove(toParent: nil) controller.view.removeFromSuperview() controller.removeFromParent() self?.contactPickerViewReference = nil }
内容的提问来源于stack exchange,提问作者Mohammed Hussain V

