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.
Możesz używać interfejsu Codable do mapowania danych z interfejsu API internetowego na model danych aplikacji (i odwrotnie), ale ma on znacznie większą elastyczność.
W tym przewodniku omówimy, jak za pomocą Codable można mapować dane z Cloud Firestore na typy 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).
Oczywiście nadal możesz używać słowników bezpośrednio w Swift, a ich duża elastyczność może być dokładnie tym, czego potrzebujesz. 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 Codable API w Swift Cloud Firestore to znacznie ułatwia:
- 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 Swifta.
- Ł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 zapominajmy też, że nie korzystamy z systemu silnego typowania Swift, który zna dokładny typ każdej właściwości Book
.
Co to jest Codable?
Według dokumentacji Apple Codable to „typ, który może konwertować się na zewnętrzne reprezentacje i z nich korzystać”. W istocie Codable jest aliasem typu dla protokołów Encodable i Decodable. Dzięki dopasowaniu typu Swift do tego protokołu kompilator będzie syntetyzować kod potrzebny do kodowania/dekodowania wystąpienia tego typu z serii formatu, np. 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 interfejsu Codable jest minimalnie inwazyjne. Musieliśmy tylko dodać zgodność z protokołem. Nie było potrzeby wprowadzania żadnych innych zmian.
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 na instancję 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 Swift. Zanim przejdziemy do bardziej złożonych typów danych, przyjrzyjmy się mapowaniu prostych typów danych.
Aby zmapować dokumenty Cloud Firestore na typy Swift:
- Sprawdź, czy do projektu został dodany framework
FirebaseFirestore
. Możesz użyć menedżera pakietów Swift lub CocoaPods. - Zaimportuj
FirebaseFirestore
do pliku Swift. - Dopasuj typ do
Codable
. - (Opcjonalnie, jeśli chcesz używać tego typu w widoku
List
) Dodaj do typu właściwośćid
i użyj atrybutu@DocumentID
, aby polecenie Cloud Firestore mogło zmapować tę właściwość na identyfikator dokumentu. Poniżej omówimy to bardziej szczegółowo. - Użyj
documentReference.data(as: )
, aby zmapować odwołanie do dokumentu na typ Swift. - Użyj
documentReference.setData(from: )
, aby zmapować dane z typów Swift do dokumentu Cloud Firestore. - (opcjonalne, ale zdecydowanie zalecane) Wdrożyć prawidłowe przetwarzanie błędów.
Zaktualizuj 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
.
Korzystając z poprzedniego fragmentu kodu służącego do pobierania i mapowania dokumentu, możemy zastąpić cały kod mapowania ręcznego pojedynczym 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 napisać to jeszcze bardziej zwięźle, określając typ dokumentu podczas wywołania 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. 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 mapować w dokumentach, to proste wartości, takie jak tytuł książki lub imię i nazwisko autora. A co w przypadku, 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:
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ę, że w dokumencie Cloud Firestore zdefiniowaliśmy strukturę CoverImages
dla mapy okładki. Oznaczyliśmy atrybut cover w metadanych 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 tabeli Cloud Firestore. Wszystko to działa z kodem napisanym w pierwszej sekcji.
Tablice
Czasami chcemy przechowywać w dokumencie kolekcję wartości. 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”:
W Cloud Firestore możemy to modelować 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:
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 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:
- Dzięki temu wiemy, który dokument należy zaktualizować, jeśli użytkownik wprowadzi lokalne zmiany.
- SwiftUI
List
wymaga, aby elementy byłyIdentifiable
, aby zapobiec przeskakiwaniu elementów podczas ich wstawiania.
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 poprzednim 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
Cloud Firestore ma wbudowany typ danych do obsługi dat i czasu, a dzięki obsłudze interfejsu Codable w Cloud Firestore ich używanie jest proste.
Przyjrzyjmy się temu dokumentowi, który reprezentuje matkę wszystkich języków programowania, Ada, wynalezioną w 1843 r.:
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ć tego działu dotyczącego 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 dowolnym rozproszonym systemie zegary w poszczególnych systemach 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 zapisu do bazy danych. Jeśli pole nie jest nil
, gdy wywołujesz addDocument()
lub updateData()
, funkcja Cloud Firestore pozostawi 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. Dzięki przechowywaniu danych można korzystać z wielu ciekawych funkcji. Możesz na przykład zapisać lokalizację zadania, aby aplikacja mogła przypomnieć Ci o nim, gdy dotrzesz do celu.
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żesz użyć typu GeoPoint
:
struct Office: Codable {
@DocumentID var id: String?
var name: String
var location: GeoPoint
}
Odpowiadający mu typ w Swift to CLLocationCoordinate2D
. Możemy zmapować te 2 typy 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 zastosowaniem typów wyliczeniowych jest modelowanie stanów dyskretnych. Możemy na przykład pisać 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
}
Cloud Firestore nie obsługuje natywnych typów enum (czyli nie może narzucać zestawu wartości), ale możemy wykorzystać fakt, że typy enum mogą być typowane, i wybrać typ kodowany. W tym przykładzie wybrano String
, co oznacza, że wszystkie wartości wyliczenia będą mapowane na ciągi znaków 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ższą enumerację 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 postaci Codable.
W takich przypadkach możemy skorzystać z funkcji 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:
Aby zmapować ten dokument do struktury, która ma właściwość name typu String
, musimy dodać do struktury ProgrammingLanguage
enumerację 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 kodowania. 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 chcemy wykluczyć usługę reasonWhyILoveThis
z mapowania, wystarczy ją usunąć z enumeracji 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. @ExplicitNull
daje nam pewną kontrolę nad tym, jak Swift obsługuje opcjonalne wartości podczas kodowania: oznaczając właściwość opcjonalną jako @ExplicitNull
, możemy poprosić Cloud Firestore o zapisanie tej właściwości w dokumencie z wartością null, 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 mogę mapować kolory?” to jedno z najczęstszych pytań programistów, nie tylko w przypadku Cloud Firestore, ale też mapowania między Swiftem a JSON-em. 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.
Wygląda na to, że istnieje 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 (to tylko 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)
}
}
Dzięki funkcji decoder.singleValueContainer()
możemy odkodować wartość String
na jej odpowiednik Color
, bez konieczności zagnieżdżania komponentów RGBA. Możesz też używać tych wartości w interfejsie internetowym aplikacji bez konieczności ich wcześniejszego konwertowania.
Dzięki temu możemy zaktualizować kod mapowania tagów, co ułatwia obsługę kolorów 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 aplikacji produkcyjnej 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 podczas pobierania 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 odbiorników migawkowych: możemy zarejestrować odbiornik migawkowy w zbiorze (lub zapytaniu), a Cloud Firestore będzie wywoływać naszego odbiornika 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.
Zacznij używać Codable.
Interfejs Codable API w Swift zapewnia zaawansowany i elastyczny sposób mapowania danych z serializowanych formatów do modelu danych aplikacji i z niego. Z tego przewodnika dowiesz się, jak łatwo można go używać w aplikacjach, które korzystają z Cloud Firestore jako bazy danych.
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 interfejsie Codable znajdziesz w tych materiałach:
- John Sundell napisał ciekawy artykuł o podstawach Codable.
- Jeśli wolisz książki, zapoznaj się z poradnikiem Mattta pt. „Flight School Guide to Swift Codable”.
- Na koniec Donny Wals ma całą serię artykułów na temat Codable.
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.