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:
- Sprawdź, czy do projektu dodano platformę
FirebaseFirestore
. Możesz to zrobić za pomocą menedżera pakietów Swift lub CocoaPods. - Zaimportuj
FirebaseFirestore
do pliku Swift. - Dostosuj typ do
Codable
. - (Opcjonalnie, jeśli chcesz użyć typu w widoku
List
) Dodaj do typuid
wł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. - Użyj
documentReference.data(as: )
, aby zmapować odwołanie do dokumentu na typ Swift. - Użyj
documentReference.setData(from: )
, aby mapować dane z typów Swift na dokument Cloud Firestore. - (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:
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”:
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:
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łyIdentifiable
, 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:
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 createdAt
i lastUpdatedAt
.
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:
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:
- John Sundell napisał ciekawy artykuł o podstawach protokołu Codable.
- Jeśli wolisz książki, zapoznaj się z przewodnikiem Mattta po Swift Codable.
- Na koniec Donny Wals ma całą serię artykułów o protokole Codable.
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.