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

iOS 18中如何在UIKit直接调用SwiftUI的contactAccessPicker(无额外视图层)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 16:04:54