...

суббота, 22 июля 2017 г.

Определяем номера с помощью CallKit

Когда в CRM 57000 контактов, людям совсем не хочется записывать их в айфон вручную. Надо найти решение поизящней, которое позволит не просто искать контакты в отдельном приложении, но и отображать имя человека при входящем звонке. Мы долго гуглили, а потом вспомнили про анонс фреймворка CallKit с WWDC. Информации по этой теме оказалось не так много: немногословная документация, статья на Хабре и ни одного пошагового руководства. Хочу восполнить этот пробел. На примере создания простого приложения покажу, как научить CallKit определять тысячи номеров.

Определяем один номер


Для начала попробуем определить один единственный номер.

Начнем с пустого проекта. Создадим Single View Application с именем TouchInApp.

Добавим extension для определения номеров. В меню Xcode выберите File > New > Target… В разделе Application Extension выберите Call Directory Extension, нажмите Next.


В поле Product Name введите TouchInCallExtension, нажмите Finish. В появившемся алерте нажмите Cancel.

Надеюсь, вы уже подготовили тестовый телефон, с которого будете звонить. Если нет, то сейчас самое время.

В Project navigator раскройте TouchInCallExtension и откройте CallDirectoryHandler.swift. Найдите функцию addIdentificationPhoneNumbers. Там вы увидите массивы phoneNumbers и labels. Удалите номера из phoneNumbers, впишите туда тестовый номер. Удалите содержимое массива labels, впишите туда «Test number».

У вас получится что-то вроде этого:

private func addIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) throws {
   let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 79214203692 ]
   let labels = [ "Test number" ]

   for (phoneNumber, label) in zip(phoneNumbers, labels) {
       context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
   }
}

CXCallDirectoryPhoneNumber — просто typealias для Int64. Номер должен быть в формате 7XXXXXXXXXX, то есть сначала код страны (country calling code), потом сам номер. Код России +7, поэтому в данном случае пишем 7.

Поставьте приложение на устройство и тут же закройте. В нем пока нечего делать. Зайдите в настройки телефона > Phone > Call Blocking & Identification. Найдите там приложение TouchInApp и позвольте ему определять и блокировать вызовы. Бывает, что приложение не сразу появляется в списке. В таком случае закройте настройки, откройте и закройте еще раз приложение и попробуйте снова.


Когда вы переводите Switch в состояние On, вызывается addIdentificationPhoneNumbers из ранее добавленного расширения и считывает оттуда контакты.

Позвоните с тестового номера на ваше устройство. Номер должен определиться.


Определяем тысячи номеров


Все это, конечно, здорово, но это всего лишь один номер. А в начале статьи речь шла о тысячах контактов. Очевидно, что мы не будем их все вручную переписывать в массивы phoneNumbers и labels.

Итак, контакты мы должны добавлять в расширении. Из приложения мы это сделать не можем. Мы можем лишь вызвать функцию reloadExtension, вызов которой приведет к вызову addIdentificationPhoneNumbers. О ней я расскажу чуть позже.

Так или иначе, приложение будет иметь доступ к контактам. Либо они сразу будут с ним поставляться в определенном формате, либо мы будем получать их по запросу к API, либо как-то еще — неважно. Важно, что расширение должно каким-то образом получить эти контакты.

Давайте на секунду отвлечемся и проведем небольшую аналогию. Представьте, что у вас есть кот. Если есть, можете не представлять. Вы просыпаетесь утром и собираетесь его покормить. Как вы будете это делать? По всей вероятности, насыпете корм в миску. А уже из нее кот покушает.

А теперь представьте, что Call Directory Extension — это кот, а вы — приложение. И вы хотите накормить контактами Call Directory Extension. Что в нашем случае будет исполнять роль миски, которую мы должны наполнить контактами и из которой extension впоследствии будет их потреблять? К сожалению, вариантов у нас не так много. Мы не можем использовать Core Data или SQLite, так как очень сильно ограничены в ресурсах во время работы расширения.


Когда вы редактировали функцию addIdentificationPhoneNumbers, вы наверняка заметили комментарии. Там говорится о том, что «Numbers must be provided in numerically ascending order.». Сортировка выборки из базы слишком ресурсоемка для расширения. Поэтому решение, использующее БД, нам не подходит.

Все, что нам остается — использовать файл. Для простоты реализации будем использовать текстовый файл следующего формата:


Использование этого формата не ведет к оптимальной производительности. Но это позволит сделать акцент на основных моментах, вместо того, чтобы погружаться в работу с бинарными файлами.

Увы, мы не можем просто так взять и получить доступ к одному файлу как из приложения, так и из расширения. Однако, если воспользоваться App Groups, это становится возможным.

Делимся контактами с помощью App Groups



App Group позволяет приложению и расширению получать доступ к общим данным. Более подробно написано в документации Apple. Если вы никогда с этим не работали — не страшно, сейчас я расскажу, как это настроить.

В Project navigator кликните по вашему проекту. Выберите target приложения, перейдите на вкладку Capabilities, включите App Groups. Добавьте группу «group.ru.touchin.TouchInApp». Логика тут та же, что и с bundle identifier. Просто добавьте префикс group. У меня bundle identifier — «ru.touchin.TouchInApp», соответственно, группа — «group.ru.touchin.TouchInApp».

Перейдите к target'у расширения, перейдите на вкладку Capabilities, включите App Groups. Там должна появиться группа, которую вы вводили ранее. Поставьте на ней галочку.

Если мы используем опцию «Automatically manage signing», App Groups настраиваются достаточно легко. Как видите, я уложился в пару абзацев. Благодаря этому я могу не превращать статью о CallKit в статью об App Groups. Но если вы используете профайлы из аккаунта разработчика, то нужно в аккаунте добавить App Group и включить ее в App ID приложения и расширения.

Записываем контакты в файл


После включения App Group можем получить доступ к контейнеру, в котором будет храниться наш файл. Делается это следующим образом:
let container = FileManager.default
   .containerURL(forSecurityApplicationGroupIdentifier: "group.ru.touchin.TouchInApp")

«group.ru.touchin.TouchInApp» — это наша App Group, которую мы только что добавили.

Назовем наш файл «contacts» и сформируем для него URL:

guard let fileUrl = FileManager.default
   .containerURL(forSecurityApplicationGroupIdentifier: "group.ru.touchin.TouchInApp")?
   .appendingPathComponent("contacts") else { return }

Немного позже вы увидите полный код, сейчас я просто хочу пояснить некоторые моменты.

Теперь нужно записать в него номера и имена. Предполагается, что они у вас уже подготовлены в следующем виде:

let numbers = ["79214203692",
               "79640982354",
               "79982434663"]

let labels = ["Иванов Петр Петрович",
              "Сергеев Иван Николаевич",
              "Николаев Андрей Михайлович"]

Напомню, что номера должны быть с правильным кодом страны и отсортированы в порядке возрастания.

Теперь сформируем из контактов будущее содержимое файла:

var string = ""
for (number, label) in zip(numbers, labels) {
   string += "\(number),\(label)\n"
}


Каждую пару номер-имя записываем в одну строку, разделяя запятой. Завершаем символом перевода строки.

Записываем все это дело в файл:

try? string.write(to: fileUrl, atomically: true, encoding: .utf8)

А теперь самое интересное. Нужно сообщить расширению, что миска наполнена и пора подкрепиться. Для этого вызовем следующую функцию:
CXCallDirectoryManager.sharedInstance.reloadExtension(
   withIdentifier: "ru.touchin.TouchInApp.TouchInCallExtension")

Параметр функции — bundle identifier расширения.

Полный код:

@IBAction func addContacts(_ sender: Any) {
    let numbers = ["79214203692",
                   "79640982354",
                   "79982434663"]
        
    let labels = ["Иванов Петр Петрович",
                  "Сергеев Иван Николаевич",
                  "Николаев Андрей Михайлович"]
        
    writeFileForCallDirectory(numbers: numbers, labels: labels)
}

private func writeFileForCallDirectory(numbers: [String], labels: [String]) {
   guard let fileUrl = FileManager.default
       .containerURL(forSecurityApplicationGroupIdentifier: "group.ru.touchin.TouchInApp")?
       .appendingPathComponent("contacts") else { return }
  
   var string = ""
   for (number, label) in zip(numbers, labels) {
       string += "\(number),\(label)\n"
   }
  
   try? string.write(to: fileUrl, atomically: true, encoding: .utf8)

   CXCallDirectoryManager.sharedInstance.reloadExtension(
      withIdentifier: "ru.touchin.TouchInApp.TouchInCallExtension")
}


Читаем контакты из файла


Но это еще не все. Мы не подготовили расширение к тому, чтобы оно могло этот файл прочесть. Попросим его читать файл по одной строчке, вычленять из строки номер и имя. Дальше поступаем так же, как с тестовым номером.

Увы, iOS не предоставляет возможность читать текстовые файлы построчно. Воспользуемся подходом, предложенным пользователем StackOverflow. Скопируйте к себе класс LineReader вместе с расширением.

Вернемся к файлу CallDirectoryHandler.swift и внесем изменения. Сначала получим URL нашего файла. Делается это точно так же, как и в приложении. Затем инициализируем LineReader путем к файлу. Читаем файл построчно и добавляем контакт за контактом.

Код обновленной функции addIdentificationPhoneNumbers:

private func addIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) throws {
   guard let fileUrl = FileManager.default
       .containerURL(forSecurityApplicationGroupIdentifier: "group.ru.touchin.TouchInApp")?
       .appendingPathComponent("contacts") else { return }
  
   guard let reader = LineReader(path: fileUrl.path) else { return }
  
   for line in reader {
       autoreleasepool {
           // удаляем перевод строки в конце
           let line = line.trimmingCharacters(in: .whitespacesAndNewlines)
          
           // отделяем номер от имени
           var components = line.components(separatedBy: ",")
          
           // приводим номер к Int64
           guard let phone = Int64(components[0]) else { return }
           let name = components[1]
          
           context.addIdentificationEntry(withNextSequentialPhoneNumber: phone, label: name)
       }
   }
}


Функция должна использовать минимум ресурсов, поэтому заверните итерацию цикла в autoreleasepool. Это позволит освобождать временные объекты и использовать меньше памяти.

Все. Теперь после вызова функции addContacts телефон будет способен определять номера из массива numbers.

Окончательную версию проекта можете скачать в репозитории на GitHub.

Что дальше?


Это лишь один из вариантов решения задачи. Его можно улучшить, используя бинарный файл вместо текстового, как сделал 2GIS. Это позволит быстрей записывать и читать данные. Соответственно, придется продумать структуру файла, а также переписать функции для записи и чтения.

Когда у вас есть представление о том, как это работает, все в ваших руках.

Комментарии (0)

    Let's block ads! (Why?)

    Комментариев нет:

    Отправить комментарий