Contacts для Swift: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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) -> Void) throws

Что делает: Перебирает все контакты, соответствующие запросу. Позволяет досрочно остановить перебор, установив 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). Это позволит не загружать сразу все контакты в память, что критично для больших адресных книг (

Рекомендаций