Mapuj dane Cloud Firestore za pomocą Swift Codable

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

Być może korzystałeś z Codable do mapowania danych z internetowego interfejsu API na model danych aplikacji (i odwrotnie), ale jest to znacznie bardziej elastyczne.

W tym przewodniku przyjrzymy się, jak można wykorzystać Codable do mapowania danych z Cloud Firestore na typy Swift i odwrotnie.

Podczas pobierania dokumentu z Cloud Firestore Twoja 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ą możesz nadal bezpośrednio korzystać ze słowników w Swift, a oferują one dużą elastyczność, która może być dokładnie tym, czego wymaga Twój przypadek użycia. Jednak to podejście nie jest bezpieczne pod względem typów i łatwo jest wprowadzić trudne do wyśledzenia błędy poprzez błędną pisownię nazw atrybutów lub zapomnienie o zmapowaniu nowego atrybutu dodanego przez zespół podczas wypuszczania tej ekscytującej nowej funkcji w zeszłym tygodniu.

W przeszłości wielu programistów obeszło te niedociągnięcia, wdrażając prostą warstwę mapowania, która umożliwiała im mapowanie słowników na typy Swift. Ale znowu większość tych implementacji opiera się na ręcznym określeniu mapowania między dokumentami Cloud Firestore a odpowiednimi typami modelu danych aplikacji.

Dzięki obsłudze Cloud Firestore dla Codable API Swifta staje się to o wiele łatwiejsze:

  • Nie będziesz już musiał ręcznie implementować żadnego kodu mapowania.
  • Łatwo jest zdefiniować sposób mapowania atrybutów o różnych nazwach.
  • Posiada wbudowaną obsługę wielu typów Swifta.
  • Łatwo jest też dodać obsługę mapowania typów niestandardowych.
  • A co najlepsze: w przypadku prostych modeli danych nie trzeba w ogóle pisać żadnego kodu mapującego.

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ć DocumentSnapshot.data() , która zwraca słownik mapujący nazwy pól na Any : func data() -> [String : Any]? .

Oznacza to, że możemy użyć składni indeksu dolnego Swifta, aby uzyskać dostęp 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)
      }
    }
  }
}

Choć kod ten może wydawać się prosty i łatwy do wdrożenia, jest delikatny, trudny w utrzymaniu i podatny na błędy.

Jak widać, przyjmujemy założenia dotyczące typów danych pól dokumentu. Mogą one być prawidłowe lub nie.

Pamiętaj, 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 w polu numberOfPages , co spowoduje trudny do znalezienia problem z mapowaniem. Ponadto będziesz musiał zaktualizować kod mapowania za każdym razem, gdy zostanie dodane nowe pole, co jest dość kłopotliwe.

I nie zapominajmy, że nie korzystamy z silnego systemu typów Swifta, który dokładnie zna właściwy typ dla każdej właściwości Book .

Co to w ogóle jest kodowalne?

Zgodnie z dokumentacją Apple, Codable to „typ, który może przekształcać się w reprezentację zewnętrzną i z niej wychodzić”. W rzeczywistości Codable jest aliasem typu dla protokołów Encodable i Decodable. Dostosowując typ Swift do tego protokołu, kompilator zsyntetyzuje kod potrzebny do zakodowania/dekodowania instancji tego typu z formatu serializowanego, takiego jak JSON.

Prosty typ przechowywania danych o książce może wyglądać następująco:

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

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

Dzięki temu możemy teraz łatwo zakodować książkę do obiektu 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 następujący sposób:

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

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

Cloud Firestore obsługuje szeroki zestaw typów danych, od prostych ciągów po zagnieżdżone mapy. Większość z nich odpowiada bezpośrednio typom wbudowanym Swifta. Przyjrzyjmy się najpierw mapowaniu kilku prostych typów danych, zanim zajmiemy się bardziej złożonymi.

Aby zmapować dokumenty Cloud Firestore na typy Swift, wykonaj następujące kroki:

  1. Upewnij się, że do swojego projektu dodałeś framework FirebaseFirestore . Możesz w tym celu użyć Menedżera pakietów Swift lub CocoaPods .
  2. Zaimportuj FirebaseFirestore do swojego pliku Swift.
  3. Dostosuj swój typ do Codable .
  4. (Opcjonalnie, jeśli chcesz użyć typu w widoku List ) Dodaj właściwość id do swojego typu i użyj @DocumentID aby poinformować Cloud Firestore o zmapowaniu tego 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 documentReference.setData(from: ) , aby zmapować dane z typów Swift na dokument Cloud Firestore.
  7. (Opcjonalne, ale wysoce zalecane) Zaimplementuj odpowiednią obsługę błędów.

Zaktualizujmy odpowiednio nasz typ Book :

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

Ponieważ ten typ był już kodowalny, musieliśmy jedynie dodać właściwość id i dodać do niej adnotację za pomocą opakowania właściwości @DocumentID .

Korzystając z poprzedniego fragmentu kodu służącego do pobierania i mapowania dokumentu, możemy zastąpić cały kod ręcznego mapowania pojedynczą linią:

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 napisać to jeszcze bardziej zwięźle, określając typ dokumentu podczas wywoływania getDocument(as:) . Spowoduje to wykonanie mapowania za Ciebie i zwrócenie typu Result zawierającego zamapowany dokument lub błąd w przypadku niepowodzenia 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 documentReference.setData(from: ) . Uwzględniając podstawową obsługę błędów, oto kod umożliwiający zapisanie instancji Book :

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 zajmie się przypisaniem nowego identyfikatora dokumentu do dokumentu. Działa to nawet wtedy, gdy aplikacja jest aktualnie 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 wiele innych typów danych, z których niektóre to typy strukturalne, których można używać do tworzenia zagnieżdżonych obiektów w dokumencie.

Zagnieżdżone typy niestandardowe

Większość atrybutów, które chcemy odwzorować w naszych dokumentach, to proste wartości, takie jak tytuł książki lub nazwisko autora. Ale co z tymi przypadkami, gdy musimy przechowywać bardziej złożony obiekt? Na przykład możemy chcieć przechowywać adresy URL okładki książki w różnych rozdzielczościach.

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

Przechowywanie zagnieżdżonego typu niestandardowego w dokumencie Firestore

Pisząc odpowiednią strukturę Swift, możemy skorzystać z faktu, że Cloud Firestore obsługuje adresy URL — przechowując pole zawierające 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ę, jak zdefiniowaliśmy strukturę CoverImages dla mapy okładki w dokumencie Cloud Firestore. Zaznaczając właściwość okładki w BookWithCoverImages jako opcjonalną, jesteśmy w stanie poradzić sobie z faktem, że niektóre dokumenty mogą nie zawierać atrybutu okładki.

Jeśli ciekawi Cię, dlaczego nie ma fragmentu kodu do pobierania lub aktualizowania danych, z przyjemnością usłyszysz, że nie ma potrzeby dostosowywania kodu do odczytu lub zapisu z/do Cloud Firestore: wszystko to działa z kodem, który my napisałem w początkowej części.

Tablice

Czasami chcemy przechowywać zbiór wartości w dokumencie. Gatunki książek są dobrym przykładem: książki takie jak Autostopem przez Galaktykę można podzielić na kilka kategorii — w tym przypadku „Sci-Fi” i „Komedia”:

Przechowywanie tablicy w dokumencie Firestore

W Cloud Firestore możemy to modelować za pomocą tablicy wartości. Jest to obsługiwane dla dowolnego typu kodowalnego (takiego jak String , Int itp.). Poniżej pokazano, jak dodać tablicę gatunków do naszego modelu Book :

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

Ponieważ działa to w przypadku dowolnego typu, który można kodować, możemy również używać typów niestandardowych. Wyobraź sobie, że chcemy przechowywać listę tagów dla każdej książki. Oprócz nazwy tagu chcielibyśmy zapisać również jego kolor, w następujący sposób:

Przechowywanie tablicy typów niestandardowych w dokumencie Firestore

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

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

I tak po prostu możemy przechowywać szereg Tags w naszych dokumentach Book !

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

Krótkie słowo na temat mapowania identyfikatorów dokumentów

Zanim przejdziemy do mapowania kolejnych typów, porozmawiajmy przez chwilę o mapowaniu identyfikatorów dokumentów.

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

  • Pomaga nam to wiedzieć, który dokument zaktualizować, jeśli użytkownik dokona lokalnych zmian.
  • List SwiftUI wymaga, aby jej elementy były możliwe do Identifiable , aby zapobiec przeskakiwaniu elementów po ich wstawieniu.

Warto zaznaczyć, że atrybut oznaczony jako @DocumentID nie będzie kodowany przez koder Cloud Firestore podczas ponownego zapisywania dokumentu. Dzieje się tak dlatego, że identyfikator dokumentu nie jest atrybutem samego dokumentu — więc zapisanie go w dokumencie byłoby błędem.

Podczas pracy z typami zagnieżdżonymi (takimi jak tablica tagów w Book we wcześniejszym przykładzie w tym przewodniku) nie jest wymagane dodawanie właściwości @DocumentID : właściwości zagnieżdżone są częścią dokumentu Cloud Firestore i nie stanowią odrębny dokument. Dlatego nie potrzebują dokumentu tożsamości.

Daty i godziny

Cloud Firestore ma wbudowany typ danych do obsługi dat i godzin, a dzięki obsłudze Codable w Cloud Firestore korzystanie z nich jest proste.

Rzućmy okiem na ten dokument, który reprezentuje matkę wszystkich języków programowania, Adę, wynalezioną w 1843 roku:

Przechowywanie dat w dokumencie Firestore

Typ Swift służący do mapowania tego dokumentu może wyglądać następująco:

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

Nie możemy opuścić tej sekcji dotyczącej dat i godzin bez rozmowy na temat @ServerTimestamp . To opakowanie właściwości jest potężnym narzędziem, jeśli chodzi o radzenie sobie ze znacznikami czasu w aplikacji.

W każdym systemie rozproszonym istnieje ryzyko, że zegary w poszczególnych systemach nie będą przez cały czas całkowicie zsynchronizowane. Możesz pomyśleć, że to nic wielkiego, ale wyobraź sobie konsekwencje nieznacznego niezsynchronizowania zegara dla systemu handlu akcjami: nawet milisekundowe odchylenie może spowodować różnicę rzędu milionów dolarów podczas realizacji transakcji.

Cloud Firestore obsługuje atrybuty oznaczone @ServerTimestamp w następujący sposób: jeśli atrybut ma wartość nil podczas jego przechowywania (na przykład za pomocą metody addDocument() ), Cloud Firestore wypełni pole bieżącym znacznikiem czasu serwera w momencie zapisywania go w bazie danych . Jeśli pole nie ma nil po wywołaniu addDocument() lub updateData() , Cloud Firestore pozostawi wartość atrybutu nietkniętą. W ten sposób można łatwo zaimplementować pola takie jak createdAt i lastUpdatedAt .

Geopunkty

Geolokalizacje są wszechobecne w naszych aplikacjach. Dzięki ich przechowywaniu możliwe staje się wiele ekscytujących funkcji. Na przykład przydatne może być zapisanie lokalizacji zadania, aby aplikacja przypominała Ci o zadaniu, gdy dotrzesz do celu.

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

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

Odpowiednim typem w Swift jest CLLocationCoordinate2D i możemy mapować pomiędzy tymi dwoma typami za pomocą następującej operacji:

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

Aby dowiedzieć się więcej na temat wysyłania zapytań do dokumentów według lokalizacji fizycznej, zapoznaj się z tym przewodnikiem po rozwiązaniach .

Wyliczenia

Wyliczenia są prawdopodobnie jedną z najbardziej niedocenianych funkcji językowych w Swift; kryje się w nich znacznie więcej, niż mogłoby się wydawać. Typowym przypadkiem użycia wyliczeń jest modelowanie dyskretnych stanów czegoś. Na przykład możemy pisać aplikację do zarządzania artykułami. Aby śledzić status artykułu, możemy użyć wyliczenia Status :

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

Cloud Firestore nie obsługuje natywnie wyliczeń (tj. nie może wymuszać zestawu wartości), ale nadal możemy skorzystać z faktu, że wyliczenia można wpisywać i wybierać typ, który można zakodować. W tym przykładzie wybraliśmy String , co oznacza, że ​​wszystkie wartości wyliczeniowe zostaną zmapowane do/z ciągu znaków, gdy będą przechowywane w dokumencie Cloud Firestore.

A ponieważ Swift obsługuje niestandardowe wartości surowe, możemy nawet dostosować, które wartości odnoszą się do danego przypadku wyliczeniowego. Na przykład, jeśli zdecydujemy się przechowywać sprawę Status.inReview jako „w przeglądzie”, możemy po prostu zaktualizować powyższe wyliczenie w następujący 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 odpowiadają nazwom 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 zdecydował się wybrać opcję Snake_case dla wszystkich nazw atrybutów.

Nie martw się: Codable nas chroni!

W takich przypadkach możemy skorzystać z CodingKeys . Jest to wyliczenie, które możemy dodać do kodowalnej struktury, aby określić sposób mapowania określonych atrybutów.

Rozważ ten dokument:

Dokument Firestore z nazwą atrybutu Snake_cased

Aby zmapować ten dokument na strukturę, która ma właściwość name typu String , musimy dodać wyliczenie CodingKeys do struktury ProgrammingLanguage 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 Codable API będzie używać nazw właściwości naszych typów Swift do określenia nazw atrybutów w dokumentach Cloud Firestore, które próbujemy zmapować. Tak długo, jak nazwy atrybutów są zgodne, nie ma potrzeby dodawania CodingKeys do naszych kodowanych typów. 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 List SwiftUI. Gdybyśmy nie określili tego w CodingKeys , nie zostałby on odwzorowany podczas pobierania danych i w ten sposób stałby się nil . Spowodowałoby to wypełnienie widoku List pierwszym dokumentem.

Każda właściwość, która nie jest wymieniona jako przypadek w odpowiednim wyliczeniu CodingKeys , zostanie zignorowana podczas procesu mapowania. Może to być naprawdę wygodne, jeśli chcemy wyraźnie wykluczyć niektóre właściwości z mapowania.

Na przykład, jeśli chcemy wykluczyć właściwość reasonWhyILoveThis z mapowania, wystarczy, że usuniemy 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 z powrotem w dokumencie Cloud Firestore. Swift ma pojęcie opcji oznaczających brak wartości, a Cloud Firestore obsługuje również wartości null . Jednak domyślnym zachowaniem przy kodowaniu opcji, które mają wartość nil , jest po prostu ich pominięcie. @ExplicitNull daje nam pewną kontrolę nad sposobem obsługi opcji Swift podczas ich kodowania: oznaczając opcjonalną właściwość jako @ExplicitNull , możemy nakazać Cloud Firestore zapisanie tej właściwości do dokumentu z wartością null, jeśli zawiera ona wartość nil .

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

Jako ostatni temat w naszym omówieniu mapowania danych za pomocą Codable, przedstawmy niestandardowe kodery i dekodery. Ta sekcja nie obejmuje natywnego typu danych Cloud Firestore, ale niestandardowe kodery i dekodery są szeroko przydatne w aplikacjach Cloud Firestore.

„Jak mogę mapować kolory” to jedno z najczęściej zadawanych pytań programistów, nie tylko w przypadku Cloud Firestore, ale także w przypadku mapowania między Swift i JSON. Istnieje wiele rozwiązań, ale większość z nich koncentruje się na JSON i prawie wszystkie odwzorowują kolory jako zagnieżdżony słownik złożony z komponentów RGB.

Wydaje się, że powinno istnieć lepsze i prostsze rozwiązanie. Dlaczego nie użyjemy kolorów internetowych (lub, ściślej mówiąc, szesnastkowego zapisu kolorów CSS) — są one łatwe w użyciu (w zasadzie tylko ciąg znaków), a nawet obsługują przezroczystość!

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

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)
  }

}

Używając decoder.singleValueContainer() , możemy zdekodować String do jego odpowiednika Color bez konieczności zagnieżdżania komponentów RGBA. Ponadto możesz używać tych wartości w interfejsie internetowym swojej aplikacji bez konieczności ich wcześniejszej konwersji!

Dzięki temu możemy zaktualizować kod mapowania tagów, ułatwiając bezpośrednią obsługę kolorów tagów zamiast konieczności ręcznego mapowania ich w kodzie interfejsu użytkownika naszej 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 aplikacji produkcyjnej należy upewnić się, że wszelkie błędy zostaną sprawnie obsłużone.

Oto fragment kodu pokazujący, jak radzić sobie z błędami, na jakie 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 demonstruje, jak obsługiwać błędy podczas pobierania pojedynczego dokumentu. Oprócz jednorazowego pobierania danych Cloud Firestore obsługuje także dostarczanie aktualizacji aplikacji na bieżąco, korzystając z tak zwanych odbiorników migawek: możemy zarejestrować odbiornik migawek w kolekcji (lub zapytaniu), a Cloud Firestore będzie dzwonić do naszego odbiornika, gdy tylko będzie to możliwe jest aktualizacja.

Oto fragment kodu pokazujący, jak zarejestrować odbiornik migawek, mapować dane za pomocą Codable i obsługiwać wszelkie błędy, które mogą wystąpić. Pokazuje także jak dodać nowy dokument do kolekcji. Jak zobaczysz, nie ma potrzeby samodzielnego aktualizowania tablicy lokalnej zawierającej zmapowane dokumenty, ponieważ zajmuje się tym kod w odbiorniku migawek.

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 .

Ruszaj i korzystaj z Codable!

Codable API firmy Swift zapewnia wydajny i elastyczny sposób mapowania danych z formatów serializowanych do i z modelu danych aplikacji. W tym przewodniku przekonałeś się, jak łatwo jest korzystać z aplikacji korzystających z Cloud Firestore jako magazynu danych.

Zaczynając od podstawowego przykładu z prostymi typami danych, stopniowo zwiększaliśmy złożoność modelu danych, cały czas mając możliwość polegania na implementacjach Codable i Firebase, które wykonały za nas mapowanie.

Aby uzyskać więcej informacji na temat Codable, polecam następujące zasoby:

Chociaż dołożyliśmy wszelkich starań, aby skompilować kompleksowy przewodnik dotyczący mapowania dokumentów Cloud Firestore, nie jest on wyczerpujący i możesz używać innych strategii do mapowania swoich typów. Korzystając z przycisku Prześlij opinię poniżej, daj nam znać, jakich strategii używasz do mapowania innych typów danych Cloud Firestore lub reprezentowania danych w Swift.

Naprawdę nie ma powodu, aby nie korzystać z obsługi Codable Cloud Firestore.