0:00
Всем привет!
Это последнее обучающее видео в первом курсе.
Мы с вами объединим полученные знания и напишем небольшое приложение.
Это простенькая книга контактов,
которая может напоминать вам о трех ближайших днях Рождения.
Помимо ручного ввода мы также задействуем системные API для доступа к записной
книжке на вашем Apple ID.
Обратите внимание: при создании плейграунда в этот раз мы выбираем MacOS —
это необходимо для того, чтобы наш код запускался на самом компьютере,
а не на симуляторе.
Таким образом, мы сможем получить доступ к записной книге,
связанной с вашим Apple ID.
Разработка приложений под компьютеры от Apple отличается от других платформ
компании.
Однако в них много общего.
В данном уроке единственное различие,
которое мы увидим — это импортированный модуль Cocoa вместо UIKit.
Cocoa содержит основной набор системных модулей,
мы еще затронем тему их содержимого в следующих курсах.
Помимо Cocoa мы импортируем еще один фреймворк — Contacts — для доступа к
контактам на устройстве пользователя.
Начнем с создания модели для хранения контакта.
Создадим протокол, требующий наличия номера телефона и его типа.
Оба свойства не опциональны,
так как телефон без указания самого номера не имеет смысла.
Создадим тип для типа телефона — его мы представим в виде перечисления.
Мы не будем полностью копировать системную книгу контактов,
дадим лишь возможность использовать три основных типа,
а для всех остальных будет использоваться значение other.
В качестве raw value мы выбрали строки,
для того чтобы было проще использовать системные контакты.
Небольшие модели данных оптимальней реализовывать в виде структур — данный
случай не исключение.
Мы хотим, чтобы наши данные являлись value type.
Так с ними будет безопаснее и удобнее работать.
Создадим структуру, реализующую протокол PhoneNumberProtocol.
Помимо телефона наши контакты будут иметь почтовый адрес.
Для простоты мы возьмем лишь улицу, город и страну.
Для начала объявим протокол с этими тремя полями,
а потом и структуру для хранения адреса.
[ШУМ] Перейдем к
самим контактам: нам нужны имя и фамилия, для того чтобы понять, чьи это данные.
Они имеют опциональный тип, так как можно создать контакт лишь с одним из полей
либо вообще создать безымянный контакт.
Также структура будет содержать массивы для хранения телефонов, адресов и имейлов.
Они не опциональны, так как просто будут пустыми,
если не указаны соответствующие данные.
Мы не стали создавать отдельный тип для хранения электронной почты ради упрощения.
Они просто будут храниться в виде массива строк.
И, конечно, нам нужно добавить дату рождения.
В системных контактах она храниться в виде компонентов,
мы тоже сделаем аналогичным образом, чтобы не конвертировать ее лишний раз.
Снова создадим структуру, просто реализующую данный протокол.
Давайте также создадим массив контактов.
Больше всего нас интересуют дни рождения,
поэтому заполним остальные поля произвольными данными.
Для проверки правильности нахождения именинников нам понадобится несколько
контактов с разными датами: те, у кого в этом году уже был день Рождения, и те,
у кого еще только будет.
При проверке логики нужно всегда помнить о крайних значениях,
поэтому мы добавим контакту, у которого день Рождения сегодня,
31 декабря, 1-го января и контакт с пустым полем birthday.
Также не забудем проверить то, что если у нескольких людей день Рождения в один и
тот же день, то они объединяются вместе.
Календарь — это вспомогательный класс,
который может преобразовывать даты в компоненты и наоборот.
Он нам пригодится при интернационализации приложения.
Не забывайте, что не все в мире пользуются григорианским календарем.
Мы не будем погружаться в эту тему,
вы можете найти необходимую информацию в документации.
Для генерации контактов создадим небольшую функцию.
Она создает контакт с переданной датой рождения,
а остальные поля заполняет стандартными данными, добавляя суффикс.
Теперь нам нужно где-то хранить все контакты.
Мы могли бы положить их просто в массив, однако лучше создать специализированный
контейнер, который будет предоставлять вспомогательные методы.
При этом будет неплохо, если контейнер будет коллекцией.
[ШУМ] Так
мы получим реализацию по умолчанию для поиска,
итерирования и других возможностей коллекции.
Создадим протокол.
Он будет наследоваться от RandomAccessCollection.
[ШУМ] Таким образом,
наша коллекция будет предоставлять произвольный доступ к элементам,
но не будет поддерживать изменения содержимого стандартными методами.
Для добавления контактов будем использовать стандартный метод.
В него мы можем добавить специальную логику, например сортировку.
Также добавим метод для получения именинников.
Он будет возвращать их в виде массива UpcomingBirthday.
UpcomingBirthday — это просто alias для кортежа,
содержащего дату и массив именинников на этот день.
Сделано это для улучшения читаемости кода.
Перейдем непосредственно к реализации записной книжки.
Мы добавим в нее функцию сортировки по имени или фамилии.
Метод sortingClosure for возвращает замыкание,
содержащего логику для соответствующего типа сортировки.
Также мы добавили метод для сравнения двух опциональных строк.
Так как логика сравнения опциональных значений неоднозначна,
ее убрали из стандартной библиотеки.
Мы хотим, чтобы не пустое значение было больше пустого,
а два не пустых сравнивались без учета регистра.
Добавим кастомный вывод в консоль.
[ШУМ] Теперь при передачи контактной
книги в функцию print будет вызываться этот метод для форматирования вывода.
Как видите, мы используем стандартный метод для коллекции — reduce,
и, как первоначальное значение, передаем некий header ContactBook.
А далее заполняем информацию для всех контактов,
просто добавляя новую информацию первоначальной строке.
Для того чтобы сделать контактную книгу коллекцией,
нужно определить всего четыре метода.
Остальные, как мы помним, имеют реализацию по умолчанию.
Переменная startIndex, которая в нашем случае возвращает 0,
переменная endIndex, которая возвращает количество элементов в нашем массиве.
А также метод index after,
который возвращает полученный индекс увеличенный на 1.
subscript же просто производит subscript по нашему внутреннему массиву.
С именинниками все интереснее.
В Swift 4 у словарей появился метод grouped by для группировки элементов по
какому-то признаку.
Однако нам не подходит словарь, нам нужна упорядоченная коллекция.
Поэтому мы добавим похожий метод в протокол Collection,
а результат будем возвращать в виде массива кортежей.
Ключи для группировки он будет получать через замыкание.
Таким образом, мы разделим логику группировки и получение ключей из
элементов, а сам метод нахождения именинников будет меньше и понятней.
Ключ будет generic-типом.
Мы сможем использовать этот метод для любых элементов и ключей.
Единственное ограничение — необходимость проверять ключи на равенство.
Алгоритм группировки довольно прост.
Сначала мы создаем пустой массив, дальше проходим по всей коллекции,
получаем ключ для очередного элемента, ищем,
есть ли у же в нашем результирующем массиве элемент по данному ключу.
Если есть, то добавляем в него новый элемент, если же его нет,
то добавляем новый массив с данным ключом.
Теперь сам метод нахождения именинников.
[ШУМ] Сначала мы берем сегодняшний день и месяц.
Дальше, используя новый метод трансформации, группируем именинников.
Ключом будет являться их день и месяц рождения.
после же сортировки мы берем тех,
у кого уже день Рождения был в этом году, и добавляем их в конец массива.
А в конце из результирующего массива берем первые три элемента.
Опробуем записную книжку в действии.
Для этого добавим в нее ранее созданные контакты.
И выведем содержимое нашей книги.
[ШУМ] Несмотря на то что
мы добавляли контакты в обратном порядке, они все равно выводятся отсортированными.
Мы можем воспользоваться стандартными методами коллекции,
например узнать ее размер или передать ее в цикл for in.
Но кому захочется вводить свою записную книгу в приложение?
Для этого есть системная API,
предоставляющая доступ к вашей записной книге на девайсе.
Так как это личная информация,
пользователь должен разрешить доступ вашему приложению.
Давайте для начала запросим доступ.
Тут следует вспомнить одну особенность плейграунда.
Его выполнение заканчивается,
как только поток управления дойдет до последней строки кода.
Запрос разрешения является асинхронной операцией,
но получение самих контактов будет ждать вашего ответа.
То есть в данном случае проблем с многопоточностью не возникнет,
однако это ненадежно, и по-хорошему нужно записать потокобезопасный код.
Мы делаем такое допущение только потому, что еще не говорили про многопоточность.
Сначала мы проверяем текущий статус.
Делать запрос необходимо, только если он равен not determined.
Соответственно, мы его получаем, проверяем на, соответственно, Determined.
И если он равен, то делаем запрос на доступ к контакту.
Как вы видите, XCode пытается получить доступ к контакту.
Разрешим ему.
Адреса и телефоны хранятся в системной адресной книге как классы CNLabeledValue.
Нам придётся воспользоваться wrapper'ом, то есть обёрткой,
так как мы не можем добавить поддержку протокола для
дженериков с определённым типом плейсхолдера.
Начнём с адреса.
Создадим переменную, которая содержит дженерик-тип CNLabeledValue и
добавим все методы из протокола: street, city и country.
С телефоном же интереснее, у него есть тип.
Мы должны подобрать соответствие системного типа телефона и нашего.
[БЕЗ_ЗВУКА] Делаем мы это следующим образом.
Пытаемся инициализировать нашу структуру PhonType с тем значением,
которое получили от системно-адресной книги.
Если инициализация удалась, то возвращаем наш тип.
Если же нет, то возвращаем тип other.
В самом контакты мы преобразуем системные телефоны и адреса через Map,
передавая в него нужный конструктор.
Так как мы не создавали отдельный тип для электронной почты,
то просто возвращаем его в виде строки.
[БЕЗ_ЗВУКА] Теперь
можно запросить контакты.
Создадим экземпляр хранилища, список интересующих нас полей и запросов.
[БЕЗ_ЗВУКА] Для
выполнения запроса передадим замыкание,
в котором добавляем все контакты в нашу книгу.
При этом метод прохода по системным контактам может выкинуть исключение,
но нам оно не интересно.
Выведем же полученный результат.
Вы можете видеть, что выполнение данной программы занимает длительное время,
так как все контакты синхронно грузятся с нашу адресную книгу.
Если же у вас вывод аналогичен предыдущему,
то проверьте, если у вас в контакте на Mac'е хоть кто-то.
Возможно, у вас отключена синхронизация с iCloud или просто ваша
адресная книга на этом Apple ID пуста.
Вы можете просто добавить несколько человек,
только не забудьте выставить им какую-нибудь дату рождения.
Теперь попробуем запросить ближайших именинников.
[БЕЗ_ЗВУКА] Для того,
чтобы красиво вывести дату, нужно использовать структуру DateFormatter.
Обычно нужно пользоваться готовыми форматами даты,
так как система будет сама выбирать нужный в зависимости от настроек девайса.
Однако, в данном случае нам не нужно выводить год — и мы составим свой формат.
Маленькая буква d — это d, большая буква M — это месяц,
а количество букв влияет на детализированность компоненты.
Всё, что осталось,
это проитерироваться по дням рождения и вывести имена всех контактов в них.
Вновь ждём выполнения
кода и наблюдаем, что к нашим тестовым
контактам добавился один реальный контакт, у которого день рождения 17 октября.
Попробуйте убрать ограничение на три дня рождения, чтобы проверить,
что алгоритм корректно обрабатывает все контакты.
Вы может загрузить проект в PlayGround на вашем компьютере,
для того чтобы ознакомиться с кодом более подробно.