Пишем фотоприложение для iOS с нуля: большой туториал
Хотите узнать, как сделать упрощенную версию Instagram? В этой статье мы создадим фотоприложение, с помощью которого вы сможете авторизоваться, добавлять фотографии и публиковать их в ленту.
Для работы нам понадобится среда разработки XCode. Код проекта лежит в репозитории на Github.
Обзор используемых библиотек
Swinject
Swinject является одним
из самых популярных фреймворков для управления зависимостями на iOS.
Наиболее распространено
использование вместе со сторибордами и расширением SwinjectStoryboard, но мы
работаем с View – слоем исключительно в коде, поэтому не будем использовать
дополнительные расширения к этому фреймворку.
Суть в следующем:
каждую зависимость
(сервис/провайдер/что угодно) мы регистрируем в контейнере, описывая в
классе-наследнике Assembly, как мы получаем экземпляр объекта-зависимости;
когда нам нужна
какая-либо зависимость, например, нам нужно передать в конструктор презентера
какой-то сервис как зависимость, мы запрашиваем эту зависимость в контейнере.
PluggableAppDelegate
Работая над разными
проектами, можно часто наблюдать перегруженный AppDelegate.swift. Он грязный, с
беспорядочными инициализациями, настройками разных библиотек, используемых в
проекте. Когда открываешь такой AppDelegate в первый раз сразу хочется его
закрыть и больше не видеть.
При использовании
PluggableAppDelegate вы разделяете AppDelegate на небольшие части, и в каждой
такой части вы описываете небольшую функциональность.
Например:
отдельный
ApplicationService (логически отделенный кусок логики AppDelegate) для
инициализации и конфигурирования пушей;
отдельный
ApplicationService для конфигурирования аналитики;
отдельный
ApplicationService для конфигурирования логирования;
и т.д.
Затем в AppDelegate вы
просто указываете, какие ApplicationService подключать.
Rx
RxSwift — FRP-фреймворк.
Добавляет возможность "реактивного стиля" программирования.
Реактивное программирование — парадигма программирования, ориентированная на
потоки данных и распространение изменений. Будем использовать Rx для биндинга
UI элементов, таких как коллекции и текстовые поля.
В этом нам поможет
расширение RxCocoa, предназначенное для работы с UI в реактивном стиле.
Firebase
Firebase — это набор
облачных сервисов от Google, такой BaaS.
На самом деле Firebase
предоставляет много возможностей, таких как:
аналитика;
аутентификация
пользователей;
база данных (облачная
и/или локальная);
хранилище;
машинное обучение;
отладка и статистика
по неполадкам;
производительность;
инструменты доставки
приложения и тестирования.
Мы используем лишь несколько
сервисов: облачную бд, хранилище для картинок наших публикаций, работу с
регистрацией и авторизацией.
Отдельно хочется
выделить еще одну библиотеку, подключаемую в этом блоке, это
FirebaseFirestoreSwift.
Это библиотека от
стороннего разработчика, она позволяет нам производить преобразования в
Firebase из Response в Codable и обратно.
EasyPeasy
Верстая UI в коде и
используя Autolayout мы должны описывать много правил позиционирования
элементов. Так вот EasyPeasy позволяет делать это кратко и элегантно.
Toast-Swift
Небольшая библиотека для
отображения сообщений пользователю в UI.
SVProgressHUD
Красивая замена
UIActivityIndicatorView с обильными возможностями для конфигурации внешнего
вида.
Paparazzo
Удобнейший пикер
изображений. Это такая мощная замена не всегда подходящему UIImagePickerController.
К тому же он локализируется и настраивается с помощью встроенных тем. А еще на
выходе Paparazzo отдает объект-обертку над ImageSource вместо UIImage, что позволяет
нам иметь больше возможностей, например, при отображении превью выбранного
изображения (установка размера изображения, заглушке). ImageSource — это такая
абстракция над UIImage.
Кроме
прочего, Paparazzo позволяет поворачивать, обрезать изображение после выбора из
галереи или снимка с камеры, а также применять фильтры на изображения.
Фильтрами мы пользоваться в рамках этого приложения не будем, но все равно
приятно, что есть запас такой функциональности.
Также
есть возможность множественного выбора изображений, отображения предварительно
выбранных изображений.
Начинаем!
Создаем пустой проект,
выбираем Single View App. Для установки всех необходимых библиотек интегрируем
CocoaPods:
Дальше переходим в папку
проекта. Инициализируем рабочее пространство для нашего приложения:
Подключаем нужные
библиотеки
Теперь в директории
проекта появился файл с ProjectName.xcworkspace, открываем его. Находим в
структуре проекта файл Podfile и начинаем заполнять нужными нам библиотеками:
Первая часть — core —
наработанный набор библиотек, ускоряющих и упрощающих разработку. Здесь
подключаются библиотеки, несущие основную функциональность приложения.
Вторая часть — firebase
services. В качестве сервиса для аналитики был выбран Firebase, позволяющий
отслеживать активность пользователей и собирать по ним аналитику. Здесь же
подгружаются все необходимые библиотеки от Google.
Третья часть — ui
— библиотека для верстки и работы с медиафайлами, в которой отображаются
уведомления. Если ищете альтернативы UIActivityIndicatorView, то вот она.
Заполним Podfile и снова
идем обратно в терминал: команда pod install добавляет все указанные нами
библиотеки.
Базовая структура проекта
Генерация модулей
Поскольку в качестве
архитектурного паттерна выбрано некое подобие MVP, используем кодогенерацию
модулей с помощью библиотеки Generamba. Generamba позволяет генерировать файлы
классов и автоматически класть в определенные папки и группы проекта.
Устанавливаем библиотеку и запускаем её:
Отвечаем на все вопросы конфигуратора:
По окончании установки
генерируется Rambafile, в который пропишем все конфигурации для генерации кода.
Файл лежит в корневой директории проекта. Должен получиться файл следующего
содержимого:
Нас интересуют только последние строки, где мы указываем название
нашего MVP шаблона модуля и откуда его качать. В качестве шаблона используем
нашу наработку:
Сохраняем файл и устанавливаем шаблоны:
Generamba подтянула
шаблон и готова генерировать заготовки кода под модули. Сделаем первый. Пусть
это будет Guest модуль — будем показывать его неавторизованным пользователям.
Там же есть описание, как подготавливать свои темплейты для
модулей. А у нас готов гостевой модуль, его мы показываем всем пользователям
при первом запуске.
Дробим AppDelegate
Первый модуль есть, но
это пока только заготовка.
Более того, этот
generamba-шаблон модуля подразумевает наличие некоторых протоколов в проекте,
например, протокола Navigator, но об этом дальше.
Сейчас займёмся
дроблением AppDelegate на отдельные сервисы.
Зачем это нужно? Это
наработанная со временем техника разделения конфигурирования разных библиотек
при запуске приложения. Зачем мешать Firebase, работу с пуш-уведомлением,
конфигурирование логгеров и много чего еще. в одном методе AppDelegate? В конце
концов, можно же использовать принцип разделения интерфейса.
В этом нам поможет
библиотека PluggableAppDelegate.
В нашем случае мы
создадим RootApplicationService для настройки рут-контроллера для window.
Также создадим
GoogleApplicationService, в котором произведём инициализацию библиотек
Firebase.
Код самого AppDelegate.swift
будет теперь выглядеть так:
Зарегистрировали сервисы
и разгрузили сам AppDelegate от кучи кода, смешанного в одном методе.
DI-контейнер
Для управления
зависимостями будем использовать Swinject.
Настало время написать
код в только что созданном RootApplicationService.
Подключаем Swinject.
Добавляем приватные
константы assembler и assemblies.
Позже в assemblies мы
будем добавлять assembly каждого нового модуля.
Пока добавим туда лишь
assembly нашего единственного Guest модуля.
Отлично! Дальше в
didFinishLaunchingWithOptions укажем ассемблеру, что нужно применить все
assemblies: это зарегистрирует наши зависимости для каждого модуля. Правда, мы
еще ни одной не написали, но скоро это исправим.
Напишем публичный метод
для перезагрузки экрана приложения — это нужно, чтобы не
держать авторизованного пользователя в стеке Guest модуля.
Пока оставим здесь
только загрузку гостевого модуля, позднее вернемся сюда и напишем логику
определения, какой модуль грузить.
Сервисы
Поскольку у нас проект
на MVP, бизнес-логику будем держать в сервисах.
Напишем первый протокол
сервиса пользователя.
Добавляем группу
/Source/Services и в нее файл UserService.swift
Сначала опишем поведение
в протоколе:
Подключим RxSwift и
Firebase
Свойство trigger нужно
нам для отправки событий о статусе авторизации пользователя, это PublishSubject
из RxSwift. Добавим немного подхода «Наблюдатель» в приложение, это
позволит писать меньше кода. Он реализует механизм, который
позволяет объекту этого класса получать оповещения об изменении состояния
других объектов и тем самым наблюдать за ними.
Добавим также Enum
AuthStatus для передачи самого статуса в триггер.
Дальше пишем реализацию
методов, описанных в протоколе.
Это будет, наверное,
самый маленький сервис в нашем приложении.
Теперь нам нужно
зарегистрировать наш сервис в DI-контейнере.
Создаем
ServicesAssembly.swift в группе Services и регистрируем наш только что
написанный сервис.
Теперь идем в RootApplicationService и добавляем в assemblies нашу ServicesAssembly перед guestAssembly.
Firebase
Для идентификации
пользователей мы используем Firebase Auth.
Давайте напишем
реализацию методов регистрации и авторизации пользователя с помощью Firebase
Auth.
Идем в наш
UserService.swift и пишем реализацию протокола сервиса.
Свойство currentUser
вычисляемое и отдает текущего пользователя, заполняя поля из объекта
пользователя Firebase, если он авторизован.
Структура пользователя
выглядит так:
Также мы добавили
свойство isFirstLaunch, говорящее нам, в первый раз запускается приложение или
нет.
Его значение мы храним
просто в UserDefaults. UserDefaults – это интерфейс, работающий с настройками
пользователя по умолчанию. Подробнее можно прочитать по теме тут.
Теперь наш файл сервиса
UserService полностью выглядит так:
Главный экран
Пришло время добавить
еще один модуль, модуль главного экрана.
Набираем в терминале уже
знакомую нам команду генерации заготовки под модуль:
Добавляем таблицу на
главный экран для отображения наших записей и UIRefreshControl, чтобы обновлять
таблицу свайпом.
Создадим структуру Post
следующего вида:
Добавим расширениям
соответствие протоколу Codable и опишем соответствие свойств объекта Post
ключам в json, который будем получать с бэкенда.
Возвращаемся в наш
HomeView, добавляем ему свойство для обновления данных с помощью RxSwift:
Теперь пропишем биндинг
таблицы и нашего поля data:
Здесь мы указываем,
какой класс ячейки использовать. В нашем случае это PostCell. Давайте посмотрим на его код:
Класс ячейки PostCell
содержит, помимо прочего, свойство value, при изменении значения которого и
происходит заполнение ячейки данными с помощью метода fillUI(post: Post).
Таким образом, как
только мы присваиваем свойству value ячейки, она заполняется, и наша таблица
отрисовывает данные.
Добавление публикации
Теперь давайте создадим
еще один модуль — экран добавления публикации. Идем в терминал и набираем:
Заготовка есть.
На этом экране добавляем
UITextField для ввода названия публикации и UIImageView для отображения превью
выбранного изображения.
Давайте пока отложим работу в модуле CreatePost и вернемся в
модуль главного экрана.
Добавим главному экрану кнопку создания
публикации.
Идем в HomeViewController и добавляем
метод setupNavigationBar:
Этот метод мы будем
вызывать во viewDidLoad, чтобы «настроить» наш UINavigationBar сразу после
того, как вью загрузилась.
В качестве селектора
указан метод addAction. Этот метод просто «говорит» презентеру, что нужно
показать медиапикер.
Дальше презентер
обращается к навигатору и сообщает ему, что нужно перейти к пикеру.
Посмотрим на код
навигатора:
Чтобы следовать
протоколу Navigator, реализуем методы makeViewController(for:) и navigate(to:)
В
createViewController(for:) проверяем destination и в случае, когда нам нужно
показать пикер, инициализируем PaparazzoViewController с настройками:
изначально нет
выбранных медиа элементов;
максимально мы можем
выбрать 1 элемент (т. е. отключаем множественный выбор).
Еще у нас есть
completion замыкание, чтобы показать пикер. Указываем выполнение этого
замыкания в блоке onFinish при инициализации PaparazzoViewController.
Дальше просто показываем
в модальном режиме наш сконфигурированный PaparazzoViewController, получаем
медиапикер с возможностью выбрать изображение из фотогалереи или сделать
снимок.
Теперь возвращаемся в
презентер главного модуля — в то место, где мы получаем сигнал от вью, что нам
нужно показать пикер, и напишем следующий код:
Здесь мы указываем, что
сразу после того, как отработал медиапикер, и у нас в completion блоке есть
items, мы выбираем первый айтем. А как мы помним, у нас медиапикер
сконфигурирован на выбор только одного изображения. Далее переходим с этим
айтемом в экран createPost.
Там подставляем
выбранное изображение в форму добавления публикации.
Нам остается только
ввести название публикации и нажать «Сохранить».
Провайдер
Мы создали структуру
Post и заполнили её данными: название и изображение.
Теперь нам нужно сохранить
нашу публикацию на бэкенде, в нашем случае в Firebase.
Для этого опишем
провайдера для работы с публикациями:
Здесь мы описали
провайдер протоколом и реализовали методы протокола в классе имплементации.
У нас получилось только
два метода: один для получения списка публикаций, второй для сохранения
публикации.
Теперь нужно
зарегистрировать наш провайдер в DI контейнере.
Создаем ProvidersAssembly:
Затем добавляем ProvidersAssembly
в список assemblies в RootApplicationService:
PostService
Теперь нам нужно
соединить только что созданный и заполненный объект публикации и провайдера для
сохранения публикации на бекенде.
Для этого нам нужен
PostService.
Как раз здесь мы и
обращаемся к провайдеру, когда нам нужно получить публикации, чтобы отобразить
их на главном экране, или когда нам нужно сохранить публикацию.
Не забываем
зарегистрировать PostService в ServicesAssembly:
Здесь у нас ещё есть
StorageService — это сервис для работы с файлами. Его код можно посмотреть в
проекте.
Итого
Мы сделали приложение, в
котором можно авторизоваться, увидеть ленту пользователя, снять фото или
загрузить из библиотеки. Его можно развивать как угодно: от допиливания
функциональности соцсетей до ухода в группировку фотографий для определенных
категорий пользователей, создания фотоальбомов, страниц пользователей и так
далее.