Mapowanie danych Cloud Firestore za pomocą Swift Codable

Interfejs Codable API w Swift 4 umożliwia wykorzystanie możliwości kompilatora, aby ułatwić mapowanie danych z formatów serializowanych na typy Swift.

Być może używasz protokołu Codable do mapowania danych z interfejsu API w internecie na model danych aplikacji (i odwrotnie), ale jest on znacznie bardziej elastyczny.

Z tego przewodnika dowiesz się, jak za pomocą protokołu Codable mapować dane z Cloud Firestore na typy Swift i odwrotnie.

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

Możesz nadal bezpośrednio używać słowników w Swift, które zapewniają dużą elastyczność, co może być dokładnie tym, czego potrzebujesz w swoim przypadku użycia. To podejście nie jest jednak bezpieczne pod względem typów i łatwo w nim wprowadzić trudne do wykrycia błędy, np. przez błędne wpisanie nazw atrybutów lub zapomnienie o zmapowaniu nowego atrybutu dodanego przez zespół w ramach nowej funkcji udostępnionej w zeszłym tygodniu.

W przeszłości wielu deweloperów radziło sobie z tymi niedociągnięciami, wdrażając prostą warstwę mapowania, która umożliwiała mapowanie słowników na typy Swift. Jednak większość tych implementacji opiera się na ręcznym określeniu mapowania między Cloud Firestoredokumentami a odpowiednimi typami modelu danych aplikacji.

Dzięki obsłudze interfejsu Codable API w Swift przez Cloud Firestore jest to znacznie łatwiejsze:

  • Nie musisz już ręcznie wdrażać żadnego kodu mapowania.
  • Łatwo określić, jak mapować atrybuty o różnych nazwach.
  • Ma wbudowaną obsługę wielu typów języka Swift.
  • Łatwo też dodać obsługę mapowania typów niestandardowych.
  • A co najważniejsze, w przypadku prostych modeli danych nie musisz pisać żadnego kodu mapowania.

Dane mapowania

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 obiekt Any: func data() -> [String : Any]?.

Oznacza to, że możemy użyć składni indeksu dolnego Swift, 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)
      }
    }
  }
}

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

Jak widać, zakładamy, że pola dokumentu zawierają określone typy danych. Mogą one być prawidłowe lub nie.

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 wykrycia problem z mapowaniem. Poza tym za każdym razem, gdy dodasz nowe pole, musisz zaktualizować kod mapowania, co jest dość uciążliwe.

Nie zapominajmy też, że nie korzystamy z silnego systemu typów w Swift, który dokładnie zna prawidłowy typ każdej właściwości Book.

Czym w ogóle jest protokół Codable?

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

Prosty typ do 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 protokołu Codable jest mało inwazyjne. Musieliśmy tylko dodać zgodność z protokołem. Nie były wymagane żadne inne zmiany.

Dzięki temu możemy teraz łatwo zakodować książkę w obiekt 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 na proste typy w dokumentach Cloud Firestore i z nich
za pomocą protokołu 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 w Swift. Zanim przejdziemy do bardziej złożonych typów danych, przyjrzyjmy się najpierw mapowaniu prostych typów danych.

Aby zmapować dokumenty Cloud Firestore na typy Swift, wykonaj te czynności:

  1. Sprawdź, czy do projektu dodano platformę FirebaseFirestore. Możesz to zrobić za pomocą menedżera pakietów Swift lub CocoaPods.
  2. Zaimportuj FirebaseFirestore do pliku Swift.
  3. Dostosuj typ do Codable.
  4. (Opcjonalnie, jeśli chcesz użyć typu w widoku List) Dodaj do typu idwłaściwość i użyj @DocumentID, aby poinformować Cloud Firestore o mapowaniu tej właściwości na identyfikator dokumentu. Omówimy to 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 mapować dane z typów Swift na dokument Cloud Firestore.
  7. (Opcjonalne, ale zdecydowanie zalecane) Wdróż odpowiednią obsługę błędów.

Zaktualizujmy odpowiednio typ Book:

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

Ten typ można było już kodować, więc wystarczyło dodać właściwość id i oznaczyć ją za pomocą opakowania właściwości @DocumentID.

W poprzednim fragmencie kodu służącym do pobierania i mapowania dokumentu możemy zastąpić cały kod mapowania ręcznego 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 zapisać jeszcze bardziej zwięźle, określając typ dokumentu podczas wywoływania funkcji getDocument(as:). Spowoduje to wykonanie mapowania i zwrócenie typu Result zawierającego zmapowany dokument lub błędu 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 funkcji documentReference.setData(from: ). Oto kod, który zapisuje instancję Book, z uwzględnieniem podstawowej obsługi 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 mu nowy identyfikator. 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 wiele innych typów danych, z których niektóre są typami strukturalnymi, których możesz używać do tworzenia zagnieżdżonych obiektów w dokumencie.

Zagnieżdżone typy niestandardowe

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

Najłatwiej zrobić to w Cloud Firestore za pomocą 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 on przekonwertowany 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 pokrycia w dokumencie Cloud Firestore. Oznaczając właściwość okładki w BookWithCoverImages jako opcjonalną, możemy uwzględnić fakt, że niektóre dokumenty mogą nie zawierać atrybutu okładki.

Jeśli zastanawiasz się, dlaczego nie ma fragmentu kodu do pobierania ani aktualizowania danych, ucieszy Cię wiadomość, że nie musisz dostosowywać kodu do odczytywania ani zapisywania danych z Cloud Firestore: wszystko to działa z kodem, który napisaliśmy w pierwszej sekcji.

Tablice

Czasami chcemy przechowywać w dokumencie kolekcję wartości. Dobrym przykładem są gatunki książek: książka taka jak Autostopem przez Galaktykę może należeć do kilku kategorii – w tym przypadku „Science fiction” i „Komedia”:

Przechowywanie tablicy w dokumencie Firestore

W Cloud Firestore możemy modelować to za pomocą tablicy wartości. Jest to obsługiwane w przypadku każdego typu, który można zakodować (np. String, Int itp.). Poniższy przykład pokazuje, jak dodać tablicę gatunków do modelu Book:

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

Działa to w przypadku każdego typu, który można zakodować, więc 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, np. w ten sposób:

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ć jej kodowanie:

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

W ten sposób 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]
}

Krótka uwaga 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 otoczki właściwości @DocumentID, aby zmapować identyfikator dokumentu Cloud Firestore na właściwość id w naszych typach Swift. Jest to ważne z kilku powodów:

  • Dzięki temu wiemy, który dokument zaktualizować, jeśli użytkownik wprowadzi lokalne zmiany.
  • List w SwiftUI wymaga, aby jego elementy były Identifiable, aby zapobiec przeskakiwaniu elementów podczas ich wstawiania.

Warto zauważyć, że atrybut oznaczony jako @DocumentID nie będzie kodowany 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.

W przypadku typów zagnieżdżonych (np. tablicy tagów w Book w przykładzie z początku tego przewodnika) nie musisz dodawać właściwości @DocumentID: zagnieżdżone właściwości są częścią dokumentu Cloud Firestore i nie stanowią osobnego dokumentu. Dlatego nie potrzebują identyfikatora dokumentu.

Daty i godziny

Cloud Firestore ma wbudowany typ danych do obsługi dat i godzin, a dzięki obsłudze protokołu Codable w Cloud Firestore można z nich łatwo korzystać.

Przyjrzyjmy się temu dokumentowi, który reprezentuje matkę wszystkich języków programowania, czyli język Ada, wynaleziony w 1843 roku:

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 pominąć tej sekcji dotyczącej dat i godzin bez rozmowy o @ServerTimestamp. Ten wrapper właściwości jest bardzo przydatny, jeśli chodzi o obsługę sygnatur czasowych w aplikacji.

W każdym systemie rozproszonym zegary poszczególnych systemów prawdopodobnie nie są przez cały czas w pełni zsynchronizowane. Może się to wydawać mało istotne, ale wyobraź sobie konsekwencje niewielkiego rozsynchronizowania zegara w systemie transakcji giełdowych: nawet milisekundowe odchylenie może spowodować różnicę w wysokości milionów dolarów podczas realizacji transakcji.

Cloud Firestore obsługuje atrybuty oznaczone symbolem @ServerTimestamp w ten sposób: jeśli atrybut jest nil podczas zapisywania go (np. za pomocą addDocument()), Cloud Firestore wypełni pole bieżącym znacznikiem czasu serwera w momencie zapisywania go w bazie danych. Jeśli pole nie jest nil podczas wywoływania addDocument() lub updateData(), Cloud Firestore pozostawi wartość atrybutu bez zmian. W ten sposób łatwo jest wdrożyć pola takie jak createdAtlastUpdatedAt.

Punkty geograficzne

Geolokalizacja jest powszechnie stosowana w naszych aplikacjach. Dzięki przechowywaniu danych możliwe jest korzystanie z wielu ciekawych funkcji. Na przykład warto zapisać lokalizację zadania, aby aplikacja mogła przypomnieć o nim, gdy dotrzesz do miejsca docelowego.

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

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

Odpowiedni typ w Swift to CLLocationCoordinate2D, a między tymi dwoma typami możemy mapować za pomocą tej operacji:

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

Więcej informacji o wykonywaniu zapytań dotyczących dokumentów według lokalizacji fizycznej znajdziesz w tym przewodniku po rozwiązaniach.

Wartości w polu enum

Typy wyliczeniowe to jedna z najbardziej niedocenianych funkcji języka Swift. Mają one znacznie więcej możliwości, niż mogłoby się wydawać. Typowe zastosowanie wyliczeń to modelowanie dyskretnych stanów czegoś. Możemy na przykład pisać aplikację do zarządzania artykułami. Aby śledzić stan 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 wyliczeń natywnie (tzn. nie może wymuszać zestawu wartości), ale możemy nadal korzystać z faktu, że wyliczenia mogą być typowane, i wybrać typ kodowalny. W tym przykładzie wybrano String, co oznacza, że wszystkie wartości wyliczeniowe będą mapowane na ciągi znaków i z nich podczas przechowywania w dokumencie Cloud Firestore.

A ponieważ Swift obsługuje niestandardowe wartości pierwotne, możemy nawet dostosować, które wartości odnoszą się do których przypadków wyliczenia. Jeśli na przykład zdecydujemy się przechowywać przypadek Status.inReview jako „w trakcie sprawdzania”, możemy zaktualizować powyższy typ wyliczeniowy w ten sposób:

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

Dostosowywanie mapowania

Czasami nazwy atrybutów w Cloud Firestore dokumentach, 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 zdecydować się na używanie notacji snake_case we wszystkich nazwach atrybutów.

Bez obaw: Codable nam pomoże.

W takich przypadkach możemy użyć CodingKeys. Jest to wyliczenie, które możemy dodać do struktury z możliwością kodowania, aby określić sposób mapowania niektórych atrybutów.

Rozważ ten dokument:

Dokument Firestore z nazwą atrybutu w formacie snake_case

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 interfejs API Codable używa nazw właściwości naszych typów Swift do określania nazw atrybutów w dokumentach Cloud Firestore, które próbujemy zmapować. Dopóki nazwy atrybutów są zgodne, nie musisz dodawać CodingKeys do naszych typów z możliwością kodowania. Gdy jednak użyjemy CodingKeys w przypadku 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. Jeśli nie określiliśmy tego w CodingKeys, nie zostanie on zmapowany podczas pobierania danych, a tym samym stanie się nil. W wyniku tego widok List zostanie wypełniony 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ć przydatne, jeśli chcesz wykluczyć niektóre obiekty z mapowania.

Jeśli na przykład chcemy wykluczyć usługę reasonWhyILoveThis z mapowania, 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 z powrotem w dokumencie Cloud Firestore. Swift ma pojęcie opcjonalnych wartości, które oznaczają brak wartości, a Cloud Firestore obsługuje też wartości null. Domyślne działanie w przypadku opcjonalnych pól z wartością nil polega na ich pomijaniu. @ExplicitNull daje nam pewną kontrolę nad tym, jak opcjonalne wartości w Swift są obsługiwane podczas kodowania: oznaczając właściwość opcjonalną jako @ExplicitNull, możemy poinformować Cloud Firestore, aby zapisywał tę właściwość w dokumencie z wartością null, jeśli zawiera ona wartość nil.

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

Na koniec omówimy mapowanie danych za pomocą protokołu Codable, a mianowicie niestandardowe kodery i dekodery. Ta sekcja nie obejmuje natywnego typu danych, ale niestandardowe kodery i dekodery są bardzo przydatne w Cloud Firestoreaplikacjach.Cloud Firestore

„Jak mogę mapować kolory?” to jedno z najczęstszych pytań deweloperów, nie tylko w przypadku Cloud Firestore, ale też w przypadku mapowania między Swiftem a JSON-em. Istnieje wiele rozwiązań, ale większość z nich koncentruje się na formacie JSON, a niemal wszystkie mapują kolory jako zagnieżdżony słownik składający się z komponentów RGB.

Wydaje się, że powinno istnieć lepsze i prostsze rozwiązanie. Dlaczego nie używamy kolorów internetowych (a dokładniej notacji szesnastkowej kolorów CSS)? Są łatwe w użyciu (w zasadzie to tylko ciąg znaków) i obsługują nawet przezroczystość.

Aby zmapować Swift Color na jego wartość szesnastkową, musimy utworzyć rozszerzenie Swift, które dodaje 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. Dodatkowo możesz używać tych wartości w interfejsie internetowym aplikacji bez konieczności ich wcześniejszego przekształcania.

Dzięki temu możemy aktualizować kod tagów mapowania, co ułatwia bezpośrednie zarządzanie kolorami tagów zamiast ręcznego mapowania ich w kodzie interfejsu 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 warto zadbać o to, aby wszystkie błędy były obsługiwane w odpowiedni sposób.

Oto fragment kodu, który pokazuje, jak radzić sobie z sytuacjami błędów, które mogą się pojawić:

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 podczas pobierania pojedynczego dokumentu. Oprócz jednorazowego pobierania danych Cloud Firestore obsługuje też dostarczanie aktualizacji do aplikacji w miarę ich pojawiania się za pomocą tzw. słuchaczy migawek: możemy zarejestrować słuchacza migawek w kolekcji (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ć odbiornik zrzutów, mapować dane za pomocą protokołu Codable i obsługiwać ewentualne błędy. Pokazuje też, jak dodać nowy dokument do kolekcji. Jak zobaczysz, nie musimy sami aktualizować lokalnej tablicy zawierającej zmapowane dokumenty, ponieważ zajmuje się tym kod w detektorze zrzutów.

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.

Zacznij korzystać z Codable!

Interfejs Codable API w Swiftcie zapewnia zaawansowany i elastyczny sposób mapowania danych z formatów serializowanych do modelu danych aplikacji i z niego. Z tego przewodnika dowiesz się, jak łatwo można używać tej funkcji w aplikacjach, które korzystają z Cloud Firestore jako magazynu danych.

Zaczęliśmy od prostego przykładu z prostymi typami danych, a następnie stopniowo zwiększaliśmy złożoność modelu danych, cały czas polegając na protokole Codable i implementacji Firebase, które wykonywały mapowanie za nas.

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

Dołożyliśmy wszelkich starań, aby przygotować wyczerpujący przewodnik po mapowaniu dokumentów Cloud Firestore, ale nie jest on kompletny i możesz używać innych strategii mapowania typów. Za pomocą 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.

Nie ma żadnego powodu, aby nie korzystać z obsługi protokołu Codable w Cloud Firestore.