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
相关产品推荐
相关产品推荐

