Mapowanie danych Cloud Firestore za pomocą Swift Codable

Interfejs Codable API w Swift, wprowadzony w wersji 4, pozwala nam wykorzystać możliwości kompilatora, aby ułatwić mapowanie danych z serializowanych formatów na typy Swift.

Do mapowania danych z internetowego interfejsu API na model danych aplikacji używano Codable (i odwrotnie), ale to rozwiązanie jest znacznie bardziej elastyczne.

W tym przewodniku wyjaśnimy, jak za pomocą Codable mapować dane z typów Cloud Firestore na Swift i odwrotnie.

Podczas pobierania dokumentu z poziomu Cloud Firestore aplikacja otrzyma słownik par klucz-wartość (lub tablicę słowników, jeśli użyjesz jednej z operacji zwracających wiele dokumentów).

Teraz z pewnością nadal możesz bezpośrednio korzystać ze słowników w tym języku. Zapewniają one dużą elastyczność, która może dokładnie odpowiadać Twoim potrzebom. Takie podejście nie jest jednak bezpieczne pod względem typów i łatwo może spowodować wprowadzenie trudnych do wykrycia błędów, np. przez błędne zapisanie nazw atrybutów lub zapomnienie o zmapowaniu nowego atrybutu dodanego przez Twój zespół w ramach tej ekscytującej nowej funkcji, która została wdrożona w zeszłym tygodniu.

W przeszłości wielu deweloperów ominęło te niedociągnięcia, stosując prostą warstwę mapowania, która umożliwiała im mapowanie słowników na typy Swift. Jednak większość z nich opiera się na ręcznym określaniu mapowania między dokumentami Cloud Firestore a odpowiednimi typami modelu danych aplikacji.

Dzięki obsłudze interfejsu Swift Codable API przez Cloud Firestore jest to dużo łatwiejsze:

  • Nie musisz już ręcznie implementować kodu mapowania.
  • Łatwo określić, jak mapować atrybuty o różnych nazwach.
  • Ma wbudowane wsparcie dla wielu typów Swift.
  • Łatwo też dodać obsługę mapowania typów niestandardowych.
  • Co najlepsze, w przypadku prostych modeli danych nie musisz pisać żadnego kodu mapowania.

Mapowanie danych

Cloud Firestore przechowuje dane w dokumentach, które mapują klucze na wartości. Aby pobrać dane z pojedynczego dokumentu, możemy wywołać funkcję DocumentSnapshot.data(), która zwraca słownik mapujący nazwy pól na Any: func data() -> [String : Any]?.

Oznacza to, że możemy używać składni podkreślenia w Swift do uzyskiwania dostępu do poszczególnych pól.

import FirebaseFirestore

#warning("DO NOT MAP YOUR DOCUMENTS MANUALLY. USE CODABLE INSTEAD.")
func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)

  docRef.getDocument { document, error in
    if let error = error as NSError? {
      self.errorMessage = "Error getting document: \(error.localizedDescription)"
    }
    else {
      if let document = document {
        let id = document.documentID
        let data = document.data()
        let title = data?["title"] as? String ?? ""
        let numberOfPages = data?["numberOfPages"] as? Int ?? 0
        let author = data?["author"] as? String ?? ""
        self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author)
      }
    }
  }
}

Chociaż może się wydawać, że jest to proste i łatwe do wdrożenia, kod jest niestabilny, trudny do utrzymania i podatny na błędy.

Jak widzisz, zakładamy typy danych pól dokumentu. Mogą one być nieprawidłowe.

Pamiętaj, że ponieważ nie ma schematu, możesz łatwo dodać nowy dokument do kolekcji i wybrać inny typ pola. Możesz przypadkowo wybrać ciąg znaków dla pola numberOfPages, co spowoduje trudny do znalezienia problem z mapowaniem. Ponadto za każdym razem, gdy dodasz nowe pole, musisz zaktualizować kod mapowania, co jest dość uciążliwe.

Nie zapominaj też, że nie korzystamy z zaawansowanego systemu typów Swift, który dokładnie zna typ każdej właściwości obiektu Book.

Co to jest Codable?

Według dokumentacji Apple Codable to „typ, który może konwertować się do i z zewnętrznej reprezentacji”. W istocie Codable jest aliasem typu dla protokołów Encodable i Decodable. Po dopasowaniu typu Swift do tego protokołu kompilator syntetyzuje kod potrzebny do kodowania/dekodowania wystąpienia tego typu z serii formatu, np. JSON.

Prosty typ przechowywania danych o książce może wyglądać tak:

struct Book: Codable {
  var title: String
  var numberOfPages: Int
  var author: String
}

Jak widać, dostosowanie typu do interfejsu Codable jest minimalnie inwazyjne. Musieliśmy tylko dodać zgodność do protokołu; nie były wymagane żadne inne zmiany.

Dzięki temu możemy teraz łatwo zakodować książkę w obiekcie JSON:

do {
  let book = Book(title: "The Hitchhiker's Guide to the Galaxy",
                  numberOfPages: 816,
                  author: "Douglas Adams")
  let encoder = JSONEncoder()
  let data = try encoder.encode(book)
} 
catch {
  print("Error when trying to encode book: \(error)")
}

Dekodowanie obiektu JSON do instancji Book działa w ten sposób:

let decoder = JSONDecoder()
let data = /* fetch data from the network */
let decodedBook = try decoder.decode(Book.self, from: data)

Mapowanie typów prostych w dokumentach Cloud Firestore za pomocą Codable

Cloud Firestore obsługuje szeroki zakres typów danych, od prostych ciągów znaków po zagnieżdżone mapy. Większość z nich odpowiada bezpośrednio wbudowanym typom danych w Swift. Zanim przejdziemy do bardziej złożonych typów danych, przyjrzyjmy się mapowaniu prostych typów danych.

Aby mapować dokumenty Cloud Firestore na typy Swift:

  1. Sprawdź, czy do projektu został dodany framework FirebaseFirestore. Aby to zrobić, możesz użyć menedżera pakietów Swift lub CocoaPods.
  2. Zaimportuj FirebaseFirestore do pliku Swift.
  3. Dopasuj typ do Codable.
  4. (Opcjonalnie, jeśli chcesz użyć typu w widoku List) Dodaj do typu właściwość id, a za pomocą @DocumentID wskaż element Cloud Firestore, aby zmapować go na identyfikator dokumentu. Omówimy to bardziej szczegółowo poniżej.
  5. Użyj documentReference.data(as: ), aby zmapować odwołanie do dokumentu na typ Swift.
  6. Użyj narzędzia documentReference.setData(from: ), aby zmapować dane z typów Swift na dokument Cloud Firestore.
  7. (opcjonalne, ale zdecydowanie zalecane) Wdrożyć prawidłowe przetwarzanie błędów.

Zaktualizujmy odpowiednio typ Book:

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

Ponieważ ten typ można było już zakodować, musieliśmy tylko dodać właściwość id i opatrzyć ją otoczką właściwości @DocumentID.

Biorąc pod uwagę poprzedni fragment kodu dotyczący pobierania i mapowania dokumentu, możemy zastąpić cały ręczny kod mapowania jednym wierszem:

func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)

  docRef.getDocument { document, error in
    if let error = error as NSError? {
      self.errorMessage = "Error getting document: \(error.localizedDescription)"
    }
    else {
      if let document = document {
        do {
          self.book = try document.data(as: Book.self)
        }
        catch {
          print(error)
        }
      }
    }
  }
}

Możesz to napisać jeszcze zwięźle, określając typ dokumentu przy wywołaniu funkcji getDocument(as:). Spowoduje to wykonanie mapowania i zwrócenie typu Result zawierającego zamapowany dokument lub błąd w przypadku nieudanego dekodowania:

private func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)
  
  docRef.getDocument(as: Book.self) { result in
    switch result {
    case .success(let book):
      // A Book value was successfully initialized from the DocumentSnapshot.
      self.book = book
      self.errorMessage = nil
    case .failure(let error):
      // A Book value could not be initialized from the DocumentSnapshot.
      self.errorMessage = "Error decoding document: \(error.localizedDescription)"
    }
  }
}

Aktualizacja istniejącego dokumentu jest tak prosta jak wywołanie funkcji documentReference.setData(from: ). Oto kod zapisywania instancji Book z podstawową obsługą błędów:

func updateBook(book: Book) {
  if let id = book.id {
    let docRef = db.collection("books").document(id)
    do {
      try docRef.setData(from: book)
    }
    catch {
      print(error)
    }
  }
}

Podczas dodawania nowego dokumentu Cloud Firestore automatycznie przypisze do niego nowy identyfikator dokumentu. Działa to nawet wtedy, gdy aplikacja jest obecnie offline.

func addBook(book: Book) {
  let collectionRef = db.collection("books")
  do {
    let newDocReference = try collectionRef.addDocument(from: self.book)
    print("Book stored with new document reference: \(newDocReference)")
  }
  catch {
    print(error)
  }
}

Oprócz mapowania prostych typów danych Cloud Firestore obsługuje też inne typy danych, z których niektóre są typami uporządkowanymi, które można wykorzystać do tworzenia zagnieżdżonych obiektów w dokumencie.

Zagnieżdżone typy niestandardowe

Większość atrybutów, które chcemy zmapować w dokumentach, to proste wartości, takie jak tytuł książki lub imię i nazwisko autora. A co z przypadkami, gdy musimy przechowywać bardziej złożony obiekt? Możemy na przykład chcieć przechowywać adresy URL okładek książek w różnych rozdzielczościach.

Najłatwiej zrobić to w Cloud Firestore, korzystając z mapy:

Przechowywanie zagnieżdżonego typu niestandardowego w dokumencie Firestore

Podczas pisania odpowiedniej struktury Swift możemy wykorzystać fakt, że Cloud Firestore obsługuje adresy URL. Podczas przechowywania pola zawierającego adres URL zostanie ono przekonwertowane na ciąg znaków i odwrotnie:

struct CoverImages: Codable {
  var small: URL
  var medium: URL
  var large: URL
}

struct BookWithCoverImages: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var cover: CoverImages?
}

Zwróć uwagę na to, jak zdefiniowaliśmy strukturę (CoverImages) mapy okładki w dokumencie Cloud Firestore. Oznaczyliśmy atrybut cover w protokole BookWithCoverImages jako opcjonalny, aby umożliwić obsługę sytuacji, w której niektóre dokumenty mogą nie zawierać atrybutu cover.

Jeśli zastanawiasz się, dlaczego nie ma fragmentu kodu służącego do pobierania ani aktualizowania danych, to ucieszy Cię na pewno wiadomość, że nie musisz dostosowywać kodu do odczytu ani zapisu z użyciem Cloud Firestore: wszystko to działa z kodem napisanym w pierwszej sekcji.

Tablice

Czasami chcemy zapisać kolekcję wartości w dokumencie. Przykładem może być gatunek książki: książka Autostopem przez galaktykę może należeć do kilku kategorii – w tym przypadku do gatunków „Fantastyka naukowa” i „Komedia”:

Przechowywanie tablicy w dokumencie Firestore

W bibliotece Cloud Firestore możemy modelować to za pomocą tablicy wartości. Jest to obsługiwane w przypadku dowolnego typu, który można zakodować (np. String, Int itd.). Poniżej pokazano, jak dodać do modelu Book tablicę gatunków:

public struct BookWithGenre: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var genres: [String]
}

Ponieważ to działa w przypadku dowolnego typu, który można zakodować, możemy też używać typów niestandardowych. Załóżmy, że chcemy przechowywać listę tagów dla każdej książki. Oprócz nazwy tagu chcemy też przechowywać jego kolor:

Przechowywanie tablicy typów niestandardowych w dokumencie Firestore

Aby przechowywać tagi w ten sposób, wystarczy zaimplementować strukturę Tag, która będzie reprezentować tag i umożliwi jego kodowanie:

struct Tag: Codable, Hashable {
  var title: String
  var color: String
}

I tak możemy przechowywać tablicę Tags w dokumentach Book!

struct BookWithTags: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var tags: [Tag]
}

Kilka słów o mapowaniu identyfikatorów dokumentów

Zanim przejdziemy do mapowania kolejnych typów, zatrzymajmy się na chwilę przy mapowaniu identyfikatorów dokumentów.

W niektórych poprzednich przykładach użyliśmy kodu właściwości @DocumentID, aby zmapować identyfikator dokumentu z dokumentów Cloud Firestore na właściwość id naszych typów Swift. Jest to ważne z kilku powodów:

  • Dzięki temu wiemy, który dokument należy zaktualizować, jeśli użytkownik wprowadzi lokalne zmiany.
  • Pole List w SwiftUI wymaga, by elementy tego typu miały wartość Identifiable. Pomaga to zapobiegać przeskakiwaniu między elementami po ich wstawieniu.

Warto zauważyć, że atrybut oznaczony jako @DocumentID nie zostanie zakodowany przez koder Cloud Firestore podczas zapisywania dokumentu. Dzieje się tak, ponieważ identyfikator dokumentu nie jest atrybutem samego dokumentu, więc zapisanie go w dokumencie byłoby błędem.

Podczas pracy z typami zagnieżdżonymi (np. tablicą tagów w Book w danym przykładzie w tym przewodniku) nie trzeba dodawać właściwości @DocumentID: właściwości zagnieżdżone są częścią dokumentu Cloud Firestore i nie stanowią osobnego dokumentu. Dlatego nie potrzebują identyfikatora dokumentu.

Daty i godziny

Usługa Cloud Firestore ma wbudowany typ danych do określania dat i godzin obsługi zamówienia, a dzięki wsparciu Cloud Firestore w Codable korzystanie z niego jest bardzo łatwe.

Spójrzmy na ten dokument, który reprezentuje matkę wszystkich języków programowania, Ada, wynalezioną w 1843 r.:

Przechowywanie dat w dokumencie Firestore

Typ Swift do mapowania tego dokumentu może wyglądać tak:

struct ProgrammingLanguage: Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
}

Nie możemy pozostawić tej sekcji dotyczącej dat i czasów bez rozmowy na temat @ServerTimestamp. Ten obiekt opakowujący jest bardzo przydatny, gdy chcesz pracować z sygnaturą czasową w aplikacji.

W każdym systemie rozproszonym zegary poszczególnych systemów nie są zawsze całkowicie zsynchronizowane. Możesz pomyśleć, że to nie ma większego znaczenia, ale wyobraź sobie konsekwencje nieznacznej rozbieżności zegara w systemie handlu akcjami: nawet odchylenie o milisekundę może skutkować różnicą rzędu milionów dolarów przy zawieraniu transakcji.

Funkcja Cloud Firestore obsługuje atrybuty oznaczone etykietą @ServerTimestamp w następujący sposób: jeśli atrybut jest oznaczony etykietą nil w momencie jego zapisania (np. za pomocą funkcji addDocument()), funkcja Cloud Firestore wypełni pole bieżącym znacznikiem czasu serwera w momencie zapisywania go do bazy danych. Jeśli podczas wywoływania funkcji addDocument() lub updateData() pole nie ma wartości nil, Cloud Firestore pozostawia wartość atrybutu bez zmian. Dzięki temu można łatwo wdrażać pola takie jak createdAt i lastUpdatedAt.

Geopoints

Geolokalizacja jest wszechobecna w naszych aplikacjach. Przechowywanie ich udostępnia wiele ciekawych funkcji. Na przykład warto zapisać lokalizację zadania, aby aplikacja przypominała Ci o nim, gdy dotrzesz do miejsca docelowego.

W typie danych Cloud Firestore jest wbudowany typ danych GeoPoint, który może przechowywać długość i szerokość geograficzną dowolnej lokalizacji. Aby zmapować lokalizacje z dokumentu Cloud Firestore do innego dokumentu Cloud Firestore, możemy użyć typu GeoPoint:

struct Office: Codable {
  @DocumentID var id: String?
  var name: String
  var location: GeoPoint
}

Odpowiedni typ w kodzie Swift to CLLocationCoordinate2D. Możemy je zmapować za pomocą tej operacji:

CLLocationCoordinate2D(latitude: office.location.latitude,
                      longitude: office.location.longitude)

Więcej informacji o wysyłaniu zapytań o dokumenty według lokalizacji fizycznej znajdziesz w tym przewodniku po rozwiązaniach.

Wartości w polu enum

Enumy są prawdopodobnie jedną z najbardziej niedocenianych funkcji językowych w Swift; skrywają w sobie znacznie więcej, niż się wydaje. Typowym przypadkiem użycia wyliczeniowych jest modelowanie określonych stanów czegoś. Możemy na przykład napisać aplikację do zarządzania artykułami. Aby śledzić stan artykułu, możemy użyć enumeracji Status:

enum Status: String, Codable {
  case draft
  case inReview
  case approved
  case published
}

Funkcja Cloud Firestore nie obsługuje natywnie wyliczenia (czyli nie może egzekwować zbioru wartości), ale nadal możemy korzystać z faktu, że wyliczenia można wpisywać, i wybrać typ do kodowania. W tym przykładzie wybrano String, co oznacza, że wszystkie wartości wyliczenia zostaną zmapowane na ciągi znaków lub z nich utworzone podczas zapisywania w dokumencie Cloud Firestore.

Ponieważ Swift obsługuje niestandardowe wartości nieprzetworzone, możemy nawet dostosować, które wartości mają się odnosić do którego przypadku enum. Jeśli na przykład zdecydujemy się przechowywać wartość Status.inReview jako „w trakcie sprawdzania”, możemy zaktualizować powyższy typ enumeracji w ten sposób:

enum Status: String, Codable {
  case draft
  case inReview = "in review"
  case approved
  case published
}

Dostosowywanie mapowania

Czasami nazwy atrybutów dokumentów Cloud Firestore, które chcemy zmapować, nie pasują do nazw właściwości w naszym modelu danych w Swift. Na przykład jeden z naszych współpracowników może być programistą Pythona i może zdecydować się na użycie formatu snake_case dla wszystkich nazw atrybutów.

Nie martw się: mamy rozwiązanie w usłudze Codable.

W takich przypadkach możemy użyć CodingKeys. Jest to typ enumeracji, który możemy dodać do struktury kodowalnej, aby określić sposób mapowania niektórych atrybutów.

Zapoznaj się z tym dokumentem:

Dokument Firestore z nazwą atrybutu snake_cased

Aby zmapować ten dokument do struktury, która ma właściwość name typu String, musimy dodać do struktury ProgrammingLanguage typ wyliczenia CodingKeys i określić nazwę atrybutu w dokumencie:

struct ProgrammingLanguage: Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
  
  enum CodingKeys: String, CodingKey {
    case id
    case name = "language_name"
    case year
  }
}

Domyślnie interfejs Codable API używa nazw właściwości naszych typów Swift, aby określić nazwy atrybutów w dokumentach Cloud Firestore, które próbujemy zmapować. Dopóki nazwy atrybutów są zgodne, nie musisz dodawać atrybutu CodingKeys do typów, które można zakodować. Jednak gdy użyjemy CodingKeys dla określonego typu, musimy dodać wszystkie nazwy właściwości, które chcemy zmapować.

W powyższym fragmencie kodu zdefiniowaliśmy właściwość id, której możemy użyć jako identyfikatora w widoku SwiftUI List. Jeśli nie określimy go w elementach CodingKeys, nie zostanie on zmapowany podczas pobierania danych i stanie się elementem nil. Spowoduje to wypełnienie widoku List pierwszym dokumentem.

Właściwości, które nie są wymienione jako wielkość liter w odpowiednim typie zbioru CodingKeys, będą ignorowane podczas procesu mapowania. Może to być przydatne, jeśli chcesz wykluczyć z mapowania niektóre właściwości.

Jeśli na przykład chcesz wykluczyć z mapowania właściwość reasonWhyILoveThis, wystarczy usunąć ją z wyliczenia CodingKeys:

struct ProgrammingLanguage: Identifiable, Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
  var reasonWhyILoveThis: String = ""
  
  enum CodingKeys: String, CodingKey {
    case id
    case name = "language_name"
    case year
  }
}

Czasami możemy chcieć zapisać pusty atrybut w dokumentie Cloud Firestore. Swift ma pojęcie opcjonalnych wartości, aby oznaczać brak wartości, a Cloud Firestore obsługuje też wartości null. Jednak domyślne zachowanie podczas kodowania opcjonalnych wartości, które mają wartość nil, to ich pominięcie. Funkcja @ExplicitNull daje nam kontrolę nad sposobem obsługi parametrów opcjonalnych Swift podczas ich kodowania. Jeśli oznaczysz opcjonalną właściwość jako @ExplicitNull, możemy wskazać, że Cloud Firestore zapisze tę właściwość w dokumencie z wartością zerową, jeśli zawiera ona wartość nil.

Używanie niestandardowego kodera i dekodera do mapowania kolorów

Ostatnim tematem w naszym omówieniu mapowania danych za pomocą Codable są niestandardowe kodery i dekodery. W tej sekcji nie omawiamy natywnego typu danych Cloud Firestore, ale niestandardowe kodery i dekodery są bardzo przydatne w aplikacjach Cloud Firestore.

„Jak mapować kolory” to jedno z najczęstszych pytań programistów – nie tylko dotyczących Cloud Firestore, ale też mapowania kolorów na Swift i JSON. Istnieje wiele rozwiązań, ale większość z nich koncentruje się na formacie JSON, a prawie wszystkie mapują kolory jako zagnieżdżony słownik złożony z komponentów RGB.

Wydaje się, że powinno być lepsze, prostsze rozwiązanie. Dlaczego nie używamy kolorów internetowych (czyli dokładnej notacji kolorów w CSS)? Są one łatwe w użyciu (w podstawie jest to po prostu ciąg znaków) i obsługują nawet przezroczystość.

Aby móc mapować Swift Color na jego wartość szesnastkową, musimy utworzyć rozszerzenie Swift, które dodaje do Color interfejs Codable.

extension Color {

 init(hex: String) {
    let rgba = hex.toRGBA()

    self.init(.sRGB,
              red: Double(rgba.r),
              green: Double(rgba.g),
              blue: Double(rgba.b),
              opacity: Double(rgba.alpha))
    }

    //... (code for translating between hex and RGBA omitted for brevity)

}

extension Color: Codable {
  
  public init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let hex = try container.decode(String.self)

    self.init(hex: hex)
  }
  
  public func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(toHex)
  }

}

Za pomocą decoder.singleValueContainer() możemy zdekodować String jako jego odpowiednik Color bez konieczności zagnieżdżania komponentów RGBA. Możesz ich też używać w interfejsie internetowym aplikacji bez konieczności ich konwertowania.

Dzięki temu możemy zaktualizować kod mapowania tagów, co ułatwi bezpośrednie zarządzanie kolorami tagów zamiast ich ręcznego mapowania w kodzie interfejsu aplikacji:

struct Tag: Codable, Hashable {
  var title: String
  var color: Color
}

struct BookWithTags: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var tags: [Tag]
}

Obsługa błędów

W powyższych fragmentach kodu celowo ograniczyliśmy obsługę błędów do minimum, ale w produkcyjnej aplikacji należy zadbać o odpowiednią obsługę wszystkich błędów.

Oto fragment kodu, który pokazuje, jak obsłużyć wszelkie błędy, na które możesz się natknąć:

class MappingSimpleTypesViewModel: ObservableObject {
  @Published var book: Book = .empty
  @Published var errorMessage: String?
  
  private var db = Firestore.firestore()
  
  func fetchAndMap() {
    fetchBook(documentId: "hitchhiker")
  }
  
  func fetchAndMapNonExisting() {
    fetchBook(documentId: "does-not-exist")
  }
  
  func fetchAndTryMappingInvalidData() {
    fetchBook(documentId: "invalid-data")
  }
  
  private func fetchBook(documentId: String) {
    let docRef = db.collection("books").document(documentId)
    
    docRef.getDocument(as: Book.self) { result in
      switch result {
      case .success(let book):
        // A Book value was successfully initialized from the DocumentSnapshot.
        self.book = book
        self.errorMessage = nil
      case .failure(let error):
        // A Book value could not be initialized from the DocumentSnapshot.
        switch error {
        case DecodingError.typeMismatch(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.valueNotFound(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.keyNotFound(_, let context):
          self.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
        case DecodingError.dataCorrupted(let key):
          self.errorMessage = "\(error.localizedDescription): \(key)"
        default:
          self.errorMessage = "Error decoding document: \(error.localizedDescription)"
        }
      }
    }
  }
}

Obsługa błędów w aktualizacjach na żywo

Poprzedni fragment kodu pokazuje, jak obsługiwać błędy przy pobieraniu pojedynczego dokumentu. Oprócz pobierania danych raz Cloud Firestore obsługuje też dostarczanie aktualizacji do aplikacji w miarę ich pojawiania się za pomocą tak zwanych słuchaczy migawkowych: możemy zarejestrować słuchacza migawkowego w zbiorze (lub zapytaniu), a Cloud Firestore będzie wywoływać naszego słuchacza za każdym razem, gdy pojawi się aktualizacja.

Oto fragment kodu, który pokazuje, jak zarejestrować słuchacza zrzutu, zmapować dane za pomocą Codable i obsługiwać ewentualne błędy. Pokazuje też, jak dodać nowy dokument do kolekcji. Jak widać, nie trzeba samodzielnie aktualizować lokalnego tabli zawierającego zmapowane dokumenty, ponieważ zajmuje się tym kod w słuchaczu zrzutu.

class MappingColorsViewModel: ObservableObject {
  @Published var colorEntries = [ColorEntry]()
  @Published var newColor = ColorEntry.empty
  @Published var errorMessage: String?
  
  private var db = Firestore.firestore()
  private var listenerRegistration: ListenerRegistration?
  
  public func unsubscribe() {
    if listenerRegistration != nil {
      listenerRegistration?.remove()
      listenerRegistration = nil
    }
  }
  
  func subscribe() {
    if listenerRegistration == nil {
      listenerRegistration = db.collection("colors")
        .addSnapshotListener { [weak self] (querySnapshot, error) in
          guard let documents = querySnapshot?.documents else {
            self?.errorMessage = "No documents in 'colors' collection"
            return
          }
          
          self?.colorEntries = documents.compactMap { queryDocumentSnapshot in
            let result = Result { try queryDocumentSnapshot.data(as: ColorEntry.self) }
            
            switch result {
            case .success(let colorEntry):
              if let colorEntry = colorEntry {
                // A ColorEntry value was successfully initialized from the DocumentSnapshot.
                self?.errorMessage = nil
                return colorEntry
              }
              else {
                // A nil value was successfully initialized from the DocumentSnapshot,
                // or the DocumentSnapshot was nil.
                self?.errorMessage = "Document doesn't exist."
                return nil
              }
            case .failure(let error):
              // A ColorEntry value could not be initialized from the DocumentSnapshot.
              switch error {
              case DecodingError.typeMismatch(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.valueNotFound(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.keyNotFound(_, let context):
                self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
              case DecodingError.dataCorrupted(let key):
                self?.errorMessage = "\(error.localizedDescription): \(key)"
              default:
                self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
              }
              return nil
            }
          }
        }
    }
  }
  
  func addColorEntry() {
    let collectionRef = db.collection("colors")
    do {
      let newDocReference = try collectionRef.addDocument(from: newColor)
      print("ColorEntry stored with new document reference: \(newDocReference)")
    }
    catch {
      print(error)
    }
  }
}

Wszystkie fragmenty kodu użyte w tym poście są częścią przykładowej aplikacji, którą możesz pobrać z tego repozytorium GitHub.

Wypróbuj Codable.

Interfejs Swift Codable API zapewnia zaawansowany i elastyczny sposób mapowania danych z formatów szeregowych na model danych aplikacji lub z niego. W tym przewodniku pokazaliśmy, jak łatwo jest używać aplikacji, których magazynem danych jest Cloud Firestore.

Zaczynając od podstawowego przykładu z prostymi typami danych, stopniowo zwiększaliśmy złożoność modelu danych, korzystając przy tym z implementacji Codable i Firebase, która wykonywała za nas mapowanie.

Więcej informacji o Codable znajdziesz w tych materiałach:

Dołożyliśmy wszelkich starań, aby stworzyć wyczerpujący przewodnik po mapowaniu dokumentów Cloud Firestore, ale nie jest on kompletny. Możesz też używać innych strategii do mapowania typów. Korzystając z przycisku Prześlij opinię poniżej, poinformuj nas, jakie strategie stosujesz do mapowania innych typów danych Cloud Firestore lub przedstawiania danych w Swift.

Nie ma powodu, dla którego nie można korzystać z pomocy dotyczącej Codable w Cloud Firestore.