Contacts для Swift: полное руководство с примерами
1. Что это и зачем нужна
Библиотека Contacts (фреймворк Contacts.framework) — это официальный API от Apple для работы с адресной книгой (контактами) на устройствах под управлением iOS, macOS, watchOS и tvOS. Она пришла на смену устаревшему AddressBook и предоставляет современный, объектно-ориентированный интерфейс для доступа к контактам пользователя. Основная задача библиотеки — унифицировать процесс чтения, создания, обновления и удаления контактов, а также управление группами контактов.
Зачем это нужно? Любое приложение, которому требуется взаимодействие с телефонной книгой (мессенджеры, CRM-системы, приложения для звонков, социальные сети), должно использовать Contacts. Библиотека позволяет безопасно запрашивать разрешения у пользователя, эффективно загружать большие объёмы данных (через постраничную загрузку и предикаты), а также работать с унифицированными контактами (когда один человек объединён из нескольких записей, например, из iCloud и Google).
В отличие от старых решений, Contacts полностью поддерживает Swift (включая async/await), работает с CNContactStore как с единой точкой входа и предоставляет иммутабельные (неизменяемые) объекты для чтения и мутабельные (изменяемые) для записи, что делает код более безопасным и предсказуемым.
2. Установка
Библиотека Contacts является частью системных фреймворков Apple. Вам не нужно устанавливать её через менеджеры пакетов (CocoaPods, SPM или Carthage) — она уже доступна на устройстве. Для её использования достаточно импортировать модуль в коде.
Важно: Для работы с контактами необходимо добавить ключ NSContactsUsageDescription в файл Info.plist вашего проекта. Без этого приложение будет аварийно завершаться при попытке доступа к хранилищу.
Команда (не требуется, но для ясности):
Так как это системный фреймворк, никаких команд pip, npm или cargo не существует. Просто добавьте импорт в файл Swift:
import Contacts
3. Быстрый старт — минимальный рабочий пример
Этот пример запрашивает разрешение на доступ к контактам и выводит в консоль имена всех контактов из адресной книги.
import Contacts
import UIKit
// 1. Создаем экземпляр хранилища
let store = CNContactStore()
// 2. Запрашиваем разрешение
store.requestAccess(for: .contacts) { granted, error in
guard granted else {
print("Доступ запрещен: \(error?.localizedDescription ?? "Неизвестная ошибка")")
return
}
// 3. Создаем запрос на выборку
let keysToFetch = [CNContactGivenNameKey, CNContactFamilyNameKey]
let request = CNContactFetchRequest(keysToFetch: keysToFetch)
// 4. Выполняем выборку
do {
try store.enumerateContacts(with: request) { contact, stopPointer in
print("Найден контакт: \(contact.givenName) \(contact.familyName)")
}
} catch {
print("Ошибка при получении контактов: \(error.localizedDescription)")
}
}
4. Основные методы/функции/классы
4.1. CNContactStore
Сигнатура: class CNContactStore : NSObject
Что делает: Центральный класс для доступа к базе данных контактов. Через него выполняются все операции: запросы, сохранение, удаление.
Пример:
let store = CNContactStore()
// Далее используем store для всех операций
4.2. requestAccess(for:completionHandler:)
Сигнатура: func requestAccess(for entityType: CNEntityType, completionHandler: @escaping (Bool, Error?) -> Void)
Что делает: Запрашивает у пользователя разрешение на доступ к контактам. Вызывается один раз; впоследствии система использует сохранённый ответ.
Пример:
store.requestAccess(for: .contacts) { granted, error in
if granted {
print("Доступ разрешен")
} else {
print("Доступ запрещен")
}
}
4.3. unifiedMeContact(withKeys:)
Сигнатура: func unifiedMeContact(withKeys keys: [CNKeyDescriptor]) throws -> CNContact
Что делает: Возвращает унифицированный контакт текущего пользователя (если он есть в приложении "Контакты" как "Моя карточка").
Пример:
let keys = [CNContactGivenNameKey, CNContactEmailAddressesKey]
do {
let myContact = try store.unifiedMeContact(withKeys: keys)
print("Мое имя: \(myContact.givenName)")
} catch {
print("Не удалось получить мою карточку: \(error)")
}
4.4. enumerateContacts(with:usingBlock:)
Сигнатура: func enumerateContacts(with fetchRequest: CNContactFetchRequest, usingBlock block: @escaping (CNContact, UnsafeMutablePointer
Что делает: Перебирает все контакты, соответствующие запросу. Позволяет досрочно остановить перебор, установив stopPointer.pointee = true.
Пример:
let keys = [CNContactGivenNameKey]
let request = CNContactFetchRequest(keysToFetch: keys)
try store.enumerateContacts(with: request) { contact, stop in
print(contact.givenName)
// Остановимся после первого контакта
stop.pointee = true
}
4.5. execute(_:)
Сигнатура: func execute(_ saveRequest: CNSaveRequest) throws
Что делает: Выполняет сохранение изменений (добавление, обновление, удаление контактов и групп), предварительно подготовленных в объекте CNSaveRequest.
Пример:
let saveRequest = CNSaveRequest()
let newContact = CNMutableContact()
newContact.givenName = "Иван"
newContact.familyName = "Петров"
saveRequest.add(newContact, toContainerWithIdentifier: nil)
try store.execute(saveRequest)
4.6. groupsMatchingPredicate(_:)
Сигнатура: func groupsMatchingPredicate(_ predicate: NSPredicate?) throws -> [CNGroup]
Что делает: Возвращает список групп, соответствующих заданному предикату. Если передать nil, вернутся все группы.
Пример:
let allGroups = try store.groupsMatchingPredicate(nil)
for group in allGroups {
print("Группа: \(group.name)")
}
4.7. CNContactFetchRequest
Сигнатура: class CNContactFetchRequest : NSObject
Что делает: Класс для настройки запроса на выборку контактов. Позволяет задать ключи для загрузки, сортировку, предикат для фильтрации и размер пакета для постраничной загрузки.
Пример:
let keys = [CNContactGivenNameKey, CNContactPhoneNumbersKey]
let request = CNContactFetchRequest(keysToFetch: keys)
request.predicate = CNContact.predicateForContacts(matchingName: "Алексей")
request.sortOrder = .givenName
request.unifyResults = true
4.8. CNContactFormatter
Сигнатура: class CNContactFormatter : NSFormatter
Что делает: Форматирует имя контакта в читаемую строку с учётом локали (например, "Иван Петров" или "Петров Иван" в зависимости от настроек языка).
Пример:
let contact = // ... получаем контакт
let fullName = CNContactFormatter.string(from: contact, style: .fullName)
print("Отформатированное имя: \(fullName ?? "Нет имени")")
4.9. CNMutableContact
Сигнатура: class CNMutableContact : CNContact
Что делает: Мутабельная версия CNContact. Используется для создания новых контактов или изменения существующих. После изменений необходимо передать его в CNSaveRequest.
Пример:
let mutableContact = CNMutableContact()
mutableContact.givenName = "Мария"
mutableContact.phoneNumbers = [CNLabeledValue(label: CNLabelPhoneNumberMobile, value: CNPhoneNumber(stringValue: "+7-900-123-45-67"))]
// Далее добавляем в saveRequest
4.10. predicateForContacts(matchingName:)
Сигнатура: class func predicateForContacts(matchingName name: String) -> NSPredicate
Что делает: Создаёт предикат для поиска контактов по имени (или фамилии). Поиск регистронезависимый.
Пример:
let predicate = CNContact.predicateForContacts(matchingName: "Сергей")
let keys = [CNContactGivenNameKey]
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keys)
for contact in contacts {
print("Найден: \(contact.givenName)")
}
5. Реальный практический пример (100-200 строк кода)
Ниже представлен полноценный пример для iOS-приложения, который ищет контакты по номеру телефона и выводит их в таблицу. Код включает обработку разрешений, асинхронную загрузку и обработку ошибок.
import UIKit
import Contacts
class ContactSearchViewController: UIViewController, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var searchTextField: UITextField!
private var contacts: [CNContact] = []
private let store = CNContactStore()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
requestContactAccess()
}
// MARK: - Разрешения
private func requestContactAccess() {
store.requestAccess(for: .contacts) { [weak self] granted, error in
DispatchQueue.main.async {
if granted {
print("Доступ получен")
} else {
let alert = UIAlertController(title: "Нет доступа",
message: "Пожалуйста, разрешите доступ к контактам в Настройках.",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self?.present(alert, animated: true)
}
}
}
}
// MARK: - Поиск по номеру телефона
@IBAction func searchButtonTapped(_ sender: UIButton) {
guard let phoneNumber = searchTextField.text, !phoneNumber.isEmpty else {
print("Введите номер телефона")
return
}
// Очищаем номер от лишних символов для поиска
let cleanedNumber = phoneNumber.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
// Ключи, которые мы хотим получить
let keys: [CNKeyDescriptor] = [
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactPhoneNumbersKey as CNKeyDescriptor
]
// Создаем предикат для поиска по номеру телефона
let predicate = CNContact.predicateForContacts(matching: CNPhoneNumber(stringValue: cleanedNumber))
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
do {
// Выполняем поиск
let foundContacts = try self?.store.unifiedContacts(matching: predicate, keysToFetch: keys) ?? []
DispatchQueue.main.async {
self?.contacts = foundContacts
self?.tableView.reloadData()
if foundContacts.isEmpty {
print("Контакты с номером \(phoneNumber) не найдены")
} else {
print("Найдено \(foundContacts.count) контактов")
}
}
} catch {
DispatchQueue.main.async {
print("Ошибка поиска: \(error.localizedDescription)")
}
}
}
}
// MARK: - UITableViewDataSource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contacts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath)
let contact = contacts[indexPath.row]
// Форматируем имя
let fullName = CNContactFormatter.string(from: contact, style: .fullName) ?? "Без имени"
cell.textLabel?.text = fullName
// Показываем первый номер телефона
if let phone = contact.phoneNumbers.first {
cell.detailTextLabel?.text = phone.value.stringValue
} else {
cell.detailTextLabel?.text = "Нет номера"
}
return cell
}
}
6. Советы по производительности и типичные ошибки
Советы по производительности:
- Запрашивайте только нужные ключи. Никогда не запрашивайте все ключи сразу (например,
[CNContactDesiredKeys]). Чем меньше ключей, тем быстрее выполняется запрос. Запрашивайте только те поля, которые собираетесь отображать. - Используйте постраничную загрузку. Установите свойство
fetchRequest.pagesize(например, 50 или 100). Это позволит не загружать сразу все контакты в память, что критично для больших адресных книг (