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

TableView展示含特殊字符联系人崩溃问题求助

解决通讯录App处理非ASCII/表情字符时的崩溃问题

你遇到的崩溃问题根源很明确:当联系人姓名的第一个字符是表情、非英语字符(比如中文)或者非ASCII特殊符号时,你代码里的filter({ $0.isASCII })会返回一个空数组,这时候强行访问[0]就会触发数组越界错误,直接导致App崩溃。

我来给你一套完整的修复方案,同时优化代码结构,避免重复逻辑:

1. 核心问题分析

原来的代码中,你通过prefix(1).uppercased().unicodeScalars.filter({ $0.isASCII }).map({ $0.value })[0]来获取字符的ASCII值,但如果第一个字符本身就不是ASCII(比如表情😀的UnicodeScalar是U+1F600,完全不在ASCII范围内),filter后的数组就是空的,访问[0]必然崩溃。

另外,你重复写了三次几乎一样的判断逻辑(分别处理姓+名、只有名、只有姓的情况),不仅冗余,还容易遗漏边界情况。

2. 修复方案:安全获取分组索引

我们可以把分组判断的逻辑抽成一个独立的辅助函数,统一处理所有情况,同时做安全校验,避免空值访问:

private func groupIndexForContact(_ contact: CNContact) -> Int {
    // 优先获取姓名的第一个有效字符:先看姓氏,再看名字
    var firstName: Character?
    if !contact.familyName.isEmpty {
        firstName = contact.familyName.first
    } else if !contact.givenName.isEmpty {
        firstName = contact.givenName.first
    }
    
    // 如果姓名完全为空,直接归到最后一组
    guard let firstChar = firstName else {
        return 26
    }
    
    // 处理一个字符对应多个UnicodeScalar的情况(比如表情),取第一个Scalar
    guard let scalar = firstChar.unicodeScalars.first else {
        return 26
    }
    
    // 定义A-Z的UnicodeScalar范围
    let aScalar = Unicode.Scalar("A")!
    let zScalar = Unicode.Scalar("Z")!
    // 转成大写后判断是否在A-Z范围内
    let upperScalar = scalar.uppercased()
    
    if upperScalar >= aScalar && upperScalar <= zScalar {
        // 计算对应字母组的索引(A→0,B→1...Z→25)
        return Int(upperScalar.value - aScalar.value)
    }
    
    // 所有非A-Z的情况(表情、非ASCII字符、数字、特殊符号)都归到最后一组
    return 26
}

可选优化:支持带重音的字母分组

如果你希望把带重音的英文字母(比如É、ñ)归到对应的普通字母组(比如E、N),可以在判断前去除字符的重音标记,修改上面的函数:

private func groupIndexForContact(_ contact: CNContact) -> Int {
    var firstName: Character?
    if !contact.familyName.isEmpty {
        firstName = contact.familyName.first
    } else if !contact.givenName.isEmpty {
        firstName = contact.givenName.first
    }
    
    guard let firstChar = firstName else {
        return 26
    }
    
    // 去除重音标记,把É转成E,ñ转成n等
    let normalizedChar = String(firstChar).applyingTransform(.stripDiacritics, reverse: false) ?? String(firstChar)
    guard let scalar = normalizedChar.unicodeScalars.first else {
        return 26
    }
    
    let aScalar = Unicode.Scalar("A")!
    let zScalar = Unicode.Scalar("Z")!
    let upperScalar = scalar.uppercased()
    
    if upperScalar >= aScalar && upperScalar <= zScalar {
        return Int(upperScalar.value - aScalar.value)
    }
    
    return 26
}

3. 重构fetchContacts函数

现在把原来重复的循环逻辑替换成调用这个辅助函数,代码会简洁很多:

private func fetchContacts(){
    let store = CNContactStore()
    store.requestAccess(for: (.contacts)) { (granted, err) in
        if let err = err{
            print("Failed to request access",err)
            return
        }
        if granted {
            print("Access granted")
            let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey]
            let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
            var contacts = [Contact]()
            do{
                try store.enumerateContacts(with: request, usingBlock: { (contact, stopPointerIfYouWantToStopEnumerating) in
                    contacts.append(Contact(contact: contact))
                })
                
                // 替换原来的重复逻辑
                contacts.forEach { contact in
                    let groupIndex = self.groupIndexForContact(contact.contact)
                    self.array2d[groupIndex].append(contact)
                }
                
                self.twoDimensionalArray.removeAll()
                for index in self.array2d.indices{
                    if self.array2d[index].count != 0{
                        let names = ExpandableNames(isExpanded: true, contacts: self.array2d[index])
                        self.twoDimensionalArray.append(names)
                    }
                }
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            } catch let err{
                print("Failed to enumerate contacts", err)
            }
        }else{
            print("Access denied")
        }
    }
}

4. 关键改进点

  • 安全访问:所有可能为空的地方都加了guard let校验,彻底避免数组越界和空值崩溃
  • 代码简洁:把重复的分组判断逻辑抽成辅助函数,后续维护更方便
  • 覆盖边界情况:处理了姓名为空、字符包含多个UnicodeScalar(表情)的极端情况
  • 扩展性强:如果后续要调整分组规则,只需要修改辅助函数即可

这样修改后,你的App就不会因为表情、非英语字符或者特殊符号的联系人而崩溃了,所有非A-Z的字符都会被正确分到最后一组。

内容的提问来源于stack exchange,提问作者user8169082

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:29:33