Meet the Contact Access Button
System Frameworks 进阶 20m

认识 ContactAccessButton:更精细的联系人权限控制

Meet the Contact Access Button

2024年6月10日

在 Apple 官方观看视频

一句话判断

iOS 18 把联系人权限从”全部给”或”全不给”改成了”按需给”,ContactAccessButton 让你在搜索流中逐个授权联系人,用户体验和隐私保护同时升级。

这场 Session 讲了什么

iOS 18 对联系人授权模型做了一个根本性的改动:引入了 Limited Access(受限访问)。用户授权时不再只有”允许”和”拒绝”两个选项,而是可以选择只分享部分联系人。授权弹窗变成了两步——第一步问是否允许访问,第二步让用户选择具体哪些联系人可以分享,或者全部开放。

这意味着联系人权限从”全有或全无”变成了”按需增量授权”。对应地,苹果推出了两个新 API:Contact Access Picker 和 Contact Access Button。Picker 是一个全屏选择器,让用户修改已授权的联系人集合。而 ContactAccessButton 更巧妙——它是一个可以嵌入你现有 UI 的 SwiftUI 组件,当用户搜索联系人时,它能展示 app 还没有权限访问的搜索结果,用户点击即可逐个授权。

ContactAccessButton 的设计哲学是”在你需要的时候才问你要”。不是一上来就请求整个通讯录权限,而是在用户明确需要某个联系人的那一刻,才请求那一个联系人的权限。这种上下文相关的授权方式让用户更放心——他们清楚地知道 app 要的是哪个联系人的信息、用在哪里。

Session 还展示了 ContactAccessButton 丰富的外观自定义能力。它不是一个死板的系统组件——通过 SwiftUI modifier,你可以调整字体(控制上行文字和操作标签的外观)、前景色(控制主文字颜色)、操作标签颜色(使用 app 的 tint color)。还有两个专用 modifier:contactAccessButtonCaption 控制当只有一个匹配结果时展示哪些联系人信息,contactAccessButtonStyle 控制联系人头像的大小。这些自定义选项让按钮可以自然地融入你的 UI,不会看起来像一个生硬的系统弹窗。

值得深挖的点

ContactAccessButton 的运行机制:隐私优先的搜索组件

ContactAccessButton 不是一个普通的按钮——它是一个系统管理的隐私边界组件。按钮内部展示的是你的 app 还没有权限访问的联系人搜索结果。这些信息对你来说是不可见的——按钮渲染的内容由系统控制,你的 app 无法读取按钮内部的联系人数据。只有当用户主动点击按钮时,系统才会把对应联系人的 identifier 传给你的 app。

这个设计解决了隐私授权中一个经典的两难问题:你想展示搜索结果来帮助用户找到想要的联系人,但展示搜索结果本身就需要访问联系人数据。苹果的解决方案是让按钮成为一个”半透明”的中间层——用户能看到搜索结果(因为系统在渲染),但 app 看不到(因为 app 只收到用户点击后的 identifier)。

按钮还有严格的可见性要求:前景和背景必须有足够的对比度,按钮不能被裁剪或遮挡,必须有足够的空间渲染。如果系统检测到按钮不满足可读性要求,点击后不会授权——这是为了防止恶意 app 把按钮藏起来诱导用户误触。

增量授权的完整生命周期

iOS 18 的联系人授权现在有四个状态:notDetermined、limited、authorized、denied。limited 是新增的。当 app 处于 limited 状态时,CNContactStore 只返回用户已经授权的联系人。你可以正常创建和修改联系人——这些操作不会受 limited 状态影响。

ContactAccessButton 最巧妙的一点是:它可以在 app 还处于 notDetermined 状态时使用。用户点击按钮时,系统自动弹出一个简化版的授权提示,请求 limited access。因为这一刻发生在用户明确搜索某个联系人之后,用户很容易理解”为什么这个 app 需要联系人权限”——授权通过率自然会更高。

Session 中还提到了 iOS 18 授权弹窗的两步设计细节。第一步是一个简洁的 alert,问用户是否愿意和 app 分享联系人。如果用户点击”Continue”,进入第二步——一个联系人选择界面,用户可以勾选具体的联系人或者选择全部开放。这个界面还特别强调了”这不是最终决定,以后可以随时修改”,降低了用户的选择焦虑。最终确认页会展示所有已选中的联系人,让用户有机会反悔。整个流程的设计目标是透明和可控——用户清楚地知道自己在授权什么,也知道自己随时可以改变主意。

代码片段

在搜索流中嵌入 ContactAccessButton

struct ContactSearchResults: View {
    @Binding var searchText: String
    @State private var authStatus: CNAuthorizationStatus = .notDetermined
    
    var body: some View {
        // 先展示 app 自己数据源的结果
        ForEach(appLocalResults(searchText)) { result in
            SearchResultRow(result)
        }
        
        // 条件展示 ContactAccessButton
        if authStatus == .limited || authStatus == .notDetermined {
            ContactAccessButton(query: searchText) { identifiers in
                // 用户点击授权后,拿到联系人的 identifier
                let contacts = fetchContacts(by: identifiers)
                // 更新 UI 或执行后续操作
                dismissSearch()
            }
            // 自定义外观
            .contactAccessButtonCaption(.fullName)
            .contactAccessButtonStyle(.medium)
        }
    }
}

场景:在联系人搜索界面中展示未授权的搜索结果。坑:按钮的 callback 返回的是 [String](contact identifiers),不是 CNContact 对象,你需要自己去 CNContactStore 拉取详情。

检查和处理授权状态

let store = CNContactStore()
let status = store.authorizationStatus(for: .contacts)

switch status {
case .limited:
    // 只能访问用户授权的联系人
    let keys = [CNContactGivenNameKey, CNContactFamilyNameKey]
    let request = CNContactFetchRequest(keysToFetch: keys)
    try store.enumerateContacts(with: request) { contact, stop in
        // 只会遍历到已授权的联系人
    }
    
case .authorized:
    // 完整访问所有联系人
    break
    
case .notDetermined:
    // 还没问过用户,ContactAccessButton 会自动处理
    break
    
case .denied:
    // 用户拒绝了,引导去设置页面
    showPermissionDeniedUI()
    break
    
default:
    break
}

场景:根据不同的授权状态展示不同的 UI。坑:limited 状态下创建的新联系人不会自动加入到已授权集合中——用户需要主动授权。

用 ContactAccessPicker 管理已授权联系人

// 当用户想修改已授权的联系人集合
let picker = CNContactPickerViewController()
picker.delegate = self
// 展示联系人选择器,让用户增减已授权的联系人
present(picker, animated: true)

场景:提供一个入口让用户修改 app 可访问的联系人范围。坑:picker 是全屏的 UI,和 ContactAccessButton 的嵌入式体验相比更重,适合放在设置页面。

最佳实践

新项目:直接围绕 ContactAccessButton 设计联系人搜索流程。不要在一启动就请求整个通讯录权限——在用户搜索联系人的那一刻才通过按钮触发授权。把按钮和你的搜索结果无缝融合,让它看起来就是你 app 的一部分。用 SwiftUI modifier(contactAccessButtonCaptioncontactAccessButtonStyleforegroundStyle)自定义按钮外观。

已有项目:如果你已经用了 CNContactStore 请求了 full access,不需要马上改。但你应该检查 authorizationStatus 是否返回了新的 .limited 状态,并在 UI 上做相应处理。如果你的 app 里有联系人搜索功能,考虑把 ContactAccessButton 加进去——它能在 limited access 状态下帮你获取更多联系人的访问权限。在设置页面加一个”管理联系人访问”的入口,用 ContactAccessPicker 让用户能修改授权范围。

还有什么值得关注

  • ContactAccessButton 只响应系统验证过的触摸事件,无法通过程序化方式模拟点击——这是防篡改的安全措施。
  • 按钮内容在 app 看来是”可见但不可读”的——你的 app 知道按钮在那里,但无法读取按钮里显示的联系人信息。
  • CNContactStore 在 limited 状态下的 CNContactFetchRequest 只返回已授权的联系人,你的排序和过滤逻辑不需要改。
WWDC 2024