WWDC Quick Look 💓 By SwiftGGTeam
Meet the Contact Access Button

Meet the Contact Access Button

观看原视频

Highlight

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


核心内容

过去,app 请求通讯录权限时,用户面对的是一个”全有或全无”的选择——要么把整个通讯录交出去,要么什么都不给。这对用户来说是个艰难的决定。一个聊天 app 可能只需要联系三五个人,但为了这点需求不得不开放全部联系人。很多用户因为这个选择而直接点了”拒绝”。

iOS 18 改变了这个局面。联系人授权现在支持 Limited Access(受限访问):第一步弹窗询问是否分享联系人,第二步让用户选择具体哪些联系人可以分享,或者选择全部开放。确认页面会展示用户选中的联系人,并提示这些选择以后可以修改。授权级别从原来的三种(notDetermined / authorized / denied)扩展为四种,新增了 .limited。处于 limited 状态时,app 只能读取用户授权的那些联系人,但仍然可以修改或创建联系人。

对应这个新模型,苹果推出了两个 API。ContactAccessPicker 是一个全屏模态选择器,适合批量添加联系人的场景(比如社交 app 的好友匹配功能)。而 ContactAccessButton 更巧妙——它是一个 SwiftUI 组件,嵌入你的搜索结果列表中,展示 app 还没有权限访问的匹配联系人,用户一键即可授权。它甚至可以在 app 还处于 notDetermined 状态时使用,点击后自动弹出简化的授权请求。因为请求发生在用户明确搜索某个联系人的那一刻,用户很清楚 app 为什么要这个权限,授权通过率自然更高。

详细内容

四种授权级别

iOS 18 联系人授权有四个状态(00:33):

  • Full Access:可读写任何联系人
  • Limited Access:只能读取用户选中的联系人,但可以修改和创建
  • Not Determined:尚未授权,尝试访问 CNContactStore 时自动弹出授权弹窗
  • Denied:无法读写任何联系人数据

ContactAccessButton 基本用法

下面是官方示例代码,展示如何将 ContactAccessButton 嵌入搜索结果列表(05:15):

@Binding var searchText: String
@State var authorizationStatus: CNAuthorizationStatus = .notDetermined

var body: some View {
    List {
        ForEach(searchResults(for: searchText)) { person in
            ResultRow(person)
        }
        if authorizationStatus == .limited || authorizationStatus == .notDetermined {
            ContactAccessButton(queryString: searchText) { identifiers in
                let contacts = await fetchContacts(withIdentifiers: identifiers)
                dismissSearch(withResult: contacts)
            }
        }
    }
}

关键点:

  • searchText 绑定到搜索框的输入文本,Button 用它来匹配联系人
  • 只在 .limited.notDetermined 状态下才显示按钮——full access 时不需要,denied 时显示也没用
  • 回调参数 identifiers 是一个 [String],包含新授权的联系人标识符
  • 拿到 identifiers 后需要通过 CNContactStore 拉取完整的联系人数据

外观自定义

ContactAccessButton 支持标准 SwiftUI modifier 和两个专用 modifier(06:10):

ContactAccessButton(queryString: searchText)
  .font(.system(weight: .bold))
  .foregroundStyle(.gray)
  .tint(.green)
  .contactAccessButtonCaption(.phone)
  .contactAccessButtonStyle(ContactAccessButton.Style(imageWidth: 30))

关键点:

  • .font() 控制上行文字和操作标签的字体
  • .foregroundStyle() 控制主文字颜色
  • .tint() 控制操作标签颜色,跟随 app 的主题色
  • .contactAccessButtonCaption(.phone) 指定当只有一个匹配时展示联系人的哪个信息(如电话号码)
  • .contactAccessButtonStyle() 控制联系人头像的大小

隐私保护机制

Button 内展示的联系人信息在 app 获得权限之前是可见但私密的(07:07)。Button 只响应经过验证的交互事件。点击授权生效的前提是按钮内容清晰可读、不被遮挡——如果前景色和背景色对比度过低,或者按钮被裁剪遮挡,点击不会授予任何权限。

用 CNContactStore 获取联系人数据

ContactAccessButton 的回调只返回标识符数组,需要通过 CNContactStore 拉取完整数据(10:11):

func fetchContacts(withIdentifiers identifiers: [String]) async -> [CNContact] {
    return await Task {
        let keys = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName)]
        let fetchRequest = CNContactFetchRequest(keysToFetch: keys)
        fetchRequest.predicate = CNContact.predicateForContacts(withIdentifiers: identifiers)
        var contacts: [CNContact] = []
        do {
            try CNContactStore().enumerateContacts(with: fetchRequest) { contact, _ in
                contacts.append(contact)
            }
        } catch {
            // ...
        }
        return contacts
    }.value
}

关键点:

  • Task 包裹避免阻塞主线程
  • CNContactFetchRequest 指定要拉取的字段,这里只取了姓名相关字段
  • CNContact.predicateForContacts(withIdentifiers:) 按标识符筛选
  • enumerateContacts(with:) 遍历结果并收集到数组

contactAccessPicker

当需要批量添加联系人、而非逐个授权时,使用 contactAccessPicker(12:47):

@State private var isPresented = false

var body: some View {
    Button("Show picker") {
        isPresented.toggle()
    }.contactAccessPicker(isPresented: $isPresented) { identifiers in
        let contacts = await fetchContacts(withIdentifiers: identifiers)
        // use the new contacts!
    }
}

关键点:

  • .contactAccessPicker() modifier 绑定到一个 isPresented 状态
  • 回调只报告新增的联系人标识符,不是全部已授权的
  • 适合社交 app 中”添加更多好友”这类批量场景
  • CNContactPickerViewController 不同——后者是一次性快照,不改变 app 的授权范围

四种联系人访问方式的选择

方式适用场景是否受授权级别影响
CNContactPickerViewController一次性选人(如选邮箱、电话)不受影响,始终可用
CNContactStore读写联系人数据的主入口需要授权;limited 只能访问授权集合
ContactAccessButton搜索流中逐个授权limited 或 notDetermined 时使用
contactAccessPicker批量管理授权范围仅 limited 时使用

核心启发

  • 做什么:在联系人搜索结果中嵌入 ContactAccessButton,替代一次性请求整个通讯录。为什么值得做:用户在搜索具体联系人时授权,上下文明确,授权通过率更高。怎么开始:在搜索结果的 List 底部条件性地放置 ContactAccessButton,只在 .limited.notDetermined 状态下显示。

  • 做什么:为已有 app 适配 .limited 授权状态,在设置页提供”管理联系人访问”入口。为什么值得做:iOS 18 用户可以选择 limited access,你的 app 如果不处理这个状态,搜索结果就会缺失大量联系人,体验退化。怎么开始:检查 CNContactStore.authorizationStatus() 是否返回 .limited,在设置页添加一个按钮调用 contactAccessPicker,让用户扩展授权范围。

  • 做什么:用 SwiftUI modifier 定制 ContactAccessButton 的外观,让它融入你的 UI。为什么值得做:默认样式的按钮在视觉上可能和 app 风格割裂,用户会觉得突兀;而且对比度不足会导致按钮点击无效。怎么开始:使用 .font().foregroundStyle().tint() 匹配 app 主题色,用 .contactAccessButtonCaption(.phone) 在单匹配时展示电话号码等具体信息,确保对比度足够。

关联 Session

评论

GitHub Issues · utterances