{"id":13643,"url":"\/distributions\/13643\/click?bit=1&hash=a8215ceddd252b2083ce5ad9aec744ff1eefa9bc3de1c4cbfdb18016cc439e99","title":"\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435\u0441\u044c \u0438\u0437 \u043b\u043e\u0432\u0443\u0448\u043a\u0438 \u00ab\u043c\u043d\u043e\u0433\u043e\u0440\u0443\u043a\u043e\u0433\u043e \u0428\u0438\u0432\u044b\u00bb","buttonText":"\u041a\u0430\u043a?","imageUuid":"6f999284-e19c-51a5-b74d-c3d432185ecb","isPaidAndBannersEnabled":false}
Dmitry Molokov

Как смаппить документы Firestore с помощью Codable и не умереть

Ко мне часто обращаются стартапы с просьбой сделать MVP их проекта на iOS с простым бэкендом, например, с помощью Firebase. И самой скучной частью проекта для меня всегда является настройка маппинга.

К примеру, представим, что у нас в Firebase Firestore есть коллекция документов типа Book – в Firestore это просто key-value объект, который мы получаем от Firebase SDK в виде Dictionary.

self.books = documents.map { queryDocumentSnapshot -> Book in let data = queryDocumentSnapshot.data() let title = data["title"] as? String ?? "" let author = data["author"] as? String ?? "" let numberOfPages = data["pages"] as? Int ?? 0 return Book(id: .init(), title: title, author: author, numberOfPages: numberOfPages) }

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

Не стоит говорить, что у такого метода много недостатков:

  • Получается очень массивный код, даже для 3 атрибутов, которые у нас есть
  • Нужно «угадывать» тип вручную
  • Через пару циклов разработки в этом месте что-то неизбежно сломается

В этой статье я покажу вам, как сделать код кратким, менее подверженным ошибкам и более удобным в поддержке с помощью протокола Codable.

Что такое Codable?

Протокол Codable – это вклад Apple в стандартизованный подход к кодированию и декодированию данных. Если вы какое-то время были разработчиком Swift, вы помните, что до того, как Apple представила Codable в Swift 4, вам приходилось выполнять маппинг данных с внешними зависимостями или вручную. Благодаря Codable ничего из этого больше не требуется.

В течение долгого времени в Firestore не хватало поддержки Codable. Вот issue на GitHub с просьбой добавить поддержку сериализации объектов в Firestore # 627.

Хорошая новость заключается в том, что по состоянию на октябрь 2019 года Firestore предоставляет поддержку Codable, и он настолько прост в использовании, насколько вы могли надеяться. По сути, это сводится к трем шагам:

  • Добавьте Firestore Codable в свой проект
  • Сделайте свои модели Codable
  • Используйте новые методы для получения/хранения данных
  • Удалите весь существующий код маппинга (Бонус!)

Давайте рассмотрим немного подробнее каждый шаг

Подготавливаем проект

Поскольку поддержка Firestore Codable доступна только для Swift, она находится в отдельном модуле, FirebaseFirestoreSwift — добавьте его в свой Podfile и сделайте pod install

Делаем модели Codable

Импортируйте FirebaseFirestoreCodable в файлы, содержащие структуры ваших моделей, и реализуйте Codable, например:

struct Book: Identifiable, Codable { @DocumentID var id: String? var title: String var author: String var numberOfPages: Int // ... }

Поскольку мы хотим использовать модели Book в ListView, они должны реализовать Identifiable, то есть иметь атрибут id. Если вы раньше работали с Firestore, вы знаете, что каждый документ Firestore имеет уникальный идентификатор документа, поэтому мы можем использовать его и сопоставить его с атрибутом id. Чтобы упростить эту задачу, FirebaseFirestoreSwift предоставляет property wrapper @DocumentID, который сообщает Firestore SDK выполнить это присвоение за нас.

Если имена атрибутов в ваших документах Firestore совпадают с именами свойств структур модели, то все готово.

Однако, если имена атрибутов различаются, как в нашем примере, вам необходимо предоставить инструкции для кодера/декодера, чтобы правильно их сопоставить. Мы можем сделать это, предоставив вложенный enum, соответствующий протоколу CodingKey.

В нашем примере имя атрибута, который содержит количество страниц книги, называется pages в наших документах Firestore, но numberOfPages в нашей структуре Book. Давайте воспользуемся CodingKeys, чтобы сопоставить их друг с другом:

import Foundation import FirebaseFirestoreSwift struct Book: Identifiable, Codable { @DocumentID var id: String? var title: String var author: String var numberOfPages: Int enum CodingKeys: String, CodingKey { case id case title case author case numberOfPages = "pages" } }

Важно отметить, что после использования CodingKeys вам придется явно указать имена всех атрибутов, которые вы хотите сопоставить. Поэтому, если вы забудете сопоставить атрибут id, идентификаторы экземпляров вашей модели будут равны nil. Это приведет к неожиданному поведению, например, при попытке отобразить их в ListView. Ознакомьтесь с документацией Apple для более подробного обсуждения Codable.

Получение данных

Теперь, когда наша модель подготовлена для сопоставления, мы можем обновить существующий код сопоставления в нашей модели. На данный момент это выглядит так:

func fetchData() { db.collection("books").addSnapshotListener { (querySnapshot, error) in guard let documents = querySnapshot?.documents else { print("No documents") return } self.books = documents.map { queryDocumentSnapshot -> Book in let data = queryDocumentSnapshot.data() let title = data["title"] as? String ?? "" let author = data["author"] as? String ?? "" let numberOfPages = data["pages"] as? Int ?? 0 return Book(id: .init(), title: title, author: author, numberOfPages: numberOfPages) } }

Пора удалить код и упростить его!

Мы можем заменить код сопоставления в файле documents.map на более простую версию:

func fetchData() { db.collection("books").addSnapshotListener { (querySnapshot, error) in guard let documents = querySnapshot?.documents else { print("No documents") return } self.books = documents.compactMap { try? $0.data(as: Book.self) } } }

Как видите, мы избавились от всего процесса ручного чтения атрибутов из словаря, выполнения приведений типов и предоставления значений по умолчанию. Наш код стал намного безопаснее, поскольку SDK позаботится об этом за нас.

Все это благодаря методу data (as: ), который предоставляется модулем FirebaseFirestoreSwift. Разве не здорово удалить весь этот код?

Как записать данные?

Итак, мы рассмотрели отображение данных при чтении их из Firestore – а как насчет противоположного направления?

Оказывается, это почти так же просто, как считывание данных – давайте быстро взглянем. Для записи данных мы можем использовать addDocument (from: ) вместо .addDocument (data: ):

func addBook(book: Book) { do { let _ = try db.collection("books").addDocument(from: book) } catch { print(error) } }

Это избавляет нас от написания лишнего кода и, что более важно, гораздо меньше возможностей сделать что-то не так и внести ошибки.

Обладая основами, давайте рассмотрим еще пару дополнительных функций.

Мы уже использовали property wrapper @DocumentID, чтобы указать Firestore сопоставить идентификаторы документов с атрибутом id в нашей структуре Book. Есть два других property wrapper, которые могут оказаться полезными: @ExplicitNull и @ServerTimestamp.

Если атрибут отмечен как @ExplicitNull, Firestore запишет атрибут в целевой документ с null значением. Если вы сохраните документ с необязательным атрибутом, равным nil, в Firestore, и он не помечен как @ExplicitNull, Firestore просто пропустит его.

@ServerTimestamp полезен, если вам нужно обрабатывать временные метки в своем приложении. В любой распределенной системе есть вероятность, что часы отдельных систем не всегда полностью синхронизированы. Вы можете подумать, что в этом нет ничего страшного, но представьте себе последствия слегка рассинхронизации часов для системы торговли акциями: даже миллисекундное отклонение может привести к разнице в миллионы долларов при выполнении сделки. Firestore обрабатывает атрибуты, отмеченные @ServerTiemstamp, следующим образом: если атрибут равен nil, когда вы его сохраняете (например, с помощью addDocument), Firestore заполняет поле текущей меткой времени сервера во время записи его в базу данных. Если поле не равно nil при вызове addDocument() или updateData(), Firestore оставит значение атрибута нетронутым. Таким образом, легко реализовать такие поля, как createdAt и lastUpdatedAt.

Сегодня вы узнали, как упростить код отображения данных с помощью протокола Codable с Firestore.

Спасибо за внимание. Не забудьте заглянуть на канал Firebase на YouTube.

0
Комментарии
Читать все 0 комментариев
null