ה-Codable API של Swift, שהוצג ב-Swift 4, מאפשר לנו למנף את כוחו של המהדר כדי להקל על מיפוי נתונים מפורמטים מסודרים לסוגי Swift.
ייתכן שהשתמשת ב-Codable כדי למפות נתונים מ-API אינטרנט למודל הנתונים של האפליקציה שלך (ולהיפך), אבל זה הרבה יותר גמיש מזה.
במדריך זה, נבחן כיצד ניתן להשתמש ב-Codable כדי למפות נתונים מ-Cloud Firestore לסוגי Swift ולהיפך.
בעת שליפת מסמך מ-Cloud Firestore, האפליקציה שלך תקבל מילון של צמדי מפתח/ערך (או מערך של מילונים, אם אתה משתמש באחת מהפעולות להחזרת מסמכים מרובים).
כעת, אתה בהחלט יכול להמשיך להשתמש ישירות במילונים ב-Swift, והם מציעים גמישות רבה שעשויה להיות בדיוק מה שמצריך מקרה השימוש שלך. עם זאת, גישה זו אינה מסוג בטוח וקל להציג באגים שקשה לעקוב אחריהם על ידי איות שגוי של שמות מאפיינים, או שכחת למפות את התכונה החדשה שהצוות שלך הוסיף כשהם שלחו את התכונה החדשה והמרגשת בשבוע שעבר.
בעבר, מפתחים רבים עבדו על החסרונות הללו על ידי הטמעת שכבת מיפוי פשוטה שאפשרה להם למפות מילונים לסוגי Swift. אבל שוב, רוב ההטמעות הללו מבוססות על ציון ידני של המיפוי בין מסמכי Cloud Firestore לבין הסוגים התואמים של מודל הנתונים של האפליקציה שלך.
עם התמיכה של Cloud Firestore ב-Codable API של Swift, זה הופך להרבה יותר קל:
- לא תצטרך עוד ליישם קוד מיפוי באופן ידני.
- קל להגדיר כיצד למפות תכונות בשמות שונים.
- יש לו תמיכה מובנית עבור רבים מהסוגים של Swift.
- וקל להוסיף תמיכה למיפוי סוגים מותאמים אישית.
- הכי טוב: עבור מודלים פשוטים של נתונים, לא תצטרך לכתוב שום קוד מיפוי בכלל.
נתוני מיפוי
Cloud Firestore מאחסן נתונים במסמכים הממפים מפתחות לערכים. כדי להביא נתונים ממסמך בודד, אנו יכולים לקרוא ל- DocumentSnapshot.data()
, אשר מחזיר מילון הממפה את שמות השדות ל- Any
: func data() -> [String : Any]?
.
המשמעות היא שנוכל להשתמש בתחביר המשנה של Swift כדי לגשת לכל שדה בודד.
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)
}
}
}
}
למרות שזה עשוי להיראות פשוט וקל ליישום, הקוד הזה שביר, קשה לתחזוקה ונוטה לשגיאות.
כפי שאתה יכול לראות, אנו מניחים הנחות לגבי סוגי הנתונים של שדות המסמכים. אלה עשויים להיות נכונים או לא.
זכור, מכיוון שאין סכמה, אתה יכול בקלות להוסיף מסמך חדש לאוסף ולבחור סוג אחר לשדה. ייתכן שתבחר בטעות מחרוזת עבור השדה numberOfPages
, דבר שיוביל לבעיית מיפוי שקשה למצוא אותה. כמו כן, תצטרך לעדכן את קוד המיפוי שלך בכל פעם שמתווסף שדה חדש, וזה די מסורבל.
ובל נשכח שאנחנו לא מנצלים את מערכת הטיפוסים החזקה של סוויפט, שיודעת בדיוק את הסוג הנכון לכל אחד מהמאפיינים של Book
.
מה זה Codable, בכלל?
על פי התיעוד של אפל, Codable הוא "טיפוס שיכול להמיר את עצמו לייצוג חיצוני ומחוצה לו". למעשה, Codable הוא כינוי מסוג עבור הפרוטוקולים הניתנים לקידוד ולפענוח. על ידי התאמה של סוג Swift לפרוטוקול זה, המהדר יסנתז את הקוד הדרוש לקידוד/פענוח של מופע מסוג זה מפורמט מסודר, כגון JSON.
סוג פשוט לאחסון נתונים על ספר עשוי להיראות כך:
struct Book: Codable {
var title: String
var numberOfPages: Int
var author: String
}
כפי שאתה יכול לראות, התאמת הסוג ל-Codable היא פולשנית מינימלית. היינו צריכים רק להוסיף את ההתאמה לפרוטוקול; לא נדרשו שינויים אחרים.
כשזה קיים, אנחנו יכולים כעת לקודד ספר לאובייקט 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)")
}
פענוח אובייקט JSON למופע Book
פועל באופן הבא:
let decoder = JSONDecoder()
let data = /* fetch data from the network */
let decodedBook = try decoder.decode(Book.self, from: data)
מיפוי לסוגים פשוטים וממנו במסמכי Cloud Firestore
באמצעות Codable
Cloud Firestore תומך בקבוצה רחבה של סוגי נתונים, החל ממחרוזות פשוטות ועד מפות מקוננות. רובם מתאימים ישירות לסוגים המובנים של סוויפט. בואו נסתכל על מיפוי של כמה סוגי נתונים פשוטים תחילה לפני שנצלול אל המורכבים יותר.
כדי למפות מסמכי Cloud Firestore לסוגי Swift, בצע את השלבים הבאים:
- ודא שהוספת את מסגרת
FirebaseFirestore
לפרויקט שלך. אתה יכול להשתמש במנהל החבילות של Swift או ב-CocoaPods כדי לעשות זאת. - ייבא את
FirebaseFirestore
לקובץ Swift שלך. - התאם את הסוג שלך ל-
Codable
. - (אופציונלי, אם ברצונך להשתמש בסוג בתצוגת
List
) הוסף מאפייןid
לסוג שלך, והשתמש@DocumentID
כדי לומר ל-Cloud Firestore למפות את זה למזהה המסמך. נדון בכך ביתר פירוט להלן. - השתמש
documentReference.data(as: )
כדי למפות הפניה למסמך לסוג Swift. - השתמש
documentReference.setData(from: )
כדי למפות נתונים מסוגי Swift למסמך Cloud Firestore. - (אופציונלי, אבל מומלץ מאוד) יישם טיפול נכון בשגיאות.
בואו נעדכן את סוג Book
שלנו בהתאם:
struct Book: Codable {
@DocumentID var id: String?
var title: String
var numberOfPages: Int
var author: String
}
מכיוון שסוג זה כבר ניתן לקידוד, היינו צריכים רק להוסיף את המאפיין id
ולציין אותו במעטפת המאפיין @DocumentID
.
אם ניקח את קטע הקוד הקודם לאחזור ומיפוי מסמך, נוכל להחליף את כל קוד המיפוי הידני בשורה אחת:
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)
}
}
}
}
}
אתה יכול לכתוב זאת בצורה תמציתית עוד יותר על ידי ציון סוג המסמך בעת קריאה getDocument(as:)
. זה יבצע עבורך את המיפוי ויחזיר סוג Result
המכילה את המסמך הממוף, או שגיאה במקרה שהפענוח נכשל:
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)"
}
}
}
עדכון מסמך קיים הוא פשוט כמו קריאה ל- documentReference.setData(from: )
. כולל טיפול בשגיאות בסיסי, הנה הקוד לשמירת מופע 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)
}
}
}
בעת הוספת מסמך חדש, Cloud Firestore תדאג אוטומטית להקצות מזהה מסמך חדש למסמך. זה עובד אפילו כאשר האפליקציה כרגע במצב לא מקוון.
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)
}
}
בנוסף למיפוי סוגי נתונים פשוטים, Cloud Firestore תומך במספר סוגי נתונים אחרים, שחלקם הם סוגים מובנים שבהם אתה יכול להשתמש כדי ליצור אובייקטים מקוננים בתוך מסמך.
סוגים מותאמים אישית מקוננים
רוב התכונות שאנו רוצים למפות במסמכים שלנו הן ערכים פשוטים, כגון שם הספר או שם המחבר. אבל מה לגבי המקרים שבהם אנחנו צריכים לאחסן חפץ מורכב יותר? לדוגמה, ייתכן שנרצה לאחסן את כתובות האתרים לעטיפת הספר ברזולוציות שונות.
הדרך הקלה ביותר לעשות זאת ב-Cloud Firestore היא להשתמש במפה:
בעת כתיבת מבנה ה-Swift המתאים, אנו יכולים להשתמש בעובדה ש-Cloud Firestore תומכת בכתובות URL - כאשר מאחסנים שדה שמכיל כתובת URL, הוא יומר למחרוזת ולהיפך:
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?
}
שימו לב כיצד הגדרנו מבנה, CoverImages
, עבור מפת הכריכה במסמך Cloud Firestore. על ידי סימון מאפיין הכריכה ב- BookWithCoverImages
כאופציונלי, אנו יכולים להתמודד עם העובדה שמסמכים מסוימים עשויים שלא להכיל תכונת כיסוי.
אם אתה סקרן מדוע אין קטע קוד לאחזור או עדכון נתונים, אתה תשמח לשמוע שאין צורך להתאים את הקוד לקריאה או כתיבה מ/אל Cloud Firestore: כל זה עובד עם הקוד שאנו כתבתי בסעיף הראשוני.
מערכים
לפעמים, אנחנו רוצים לאחסן אוסף של ערכים במסמך. הז'אנרים של ספר הם דוגמה טובה: ספר כמו מדריך הטרמפיסט לגלקסיה עשוי להיכלל בכמה קטגוריות - במקרה הזה "מדע בדיוני" ו"קומדיה":
ב-Cloud Firestore, אנו יכולים לדגמן זאת באמצעות מערך של ערכים. זה נתמך עבור כל סוג שניתן לקוד (כגון String
, Int
וכו'). הבא מראה כיצד להוסיף מערך של ז'אנרים למודל Book
שלנו:
public struct BookWithGenre: Codable {
@DocumentID var id: String?
var title: String
var numberOfPages: Int
var author: String
var genres: [String]
}
מכיוון שזה עובד עבור כל סוג שניתן לקידוד, אנו יכולים להשתמש גם בסוגים מותאמים אישית. תאר לעצמך שאנחנו רוצים לאחסן רשימה של תגים עבור כל ספר. יחד עם שם התג, נרצה לאחסן גם את צבע התג, כך:
כדי לאחסן תגים בצורה זו, כל שעלינו לעשות הוא ליישם מבנה Tag
כדי לייצג תג ולהפוך אותו לקידוד:
struct Tag: Codable, Hashable {
var title: String
var color: String
}
ובדיוק ככה, אנחנו יכולים לאחסן מערך של Tags
במסמכי Book
שלנו!
struct BookWithTags: Codable {
@DocumentID var id: String?
var title: String
var numberOfPages: Int
var author: String
var tags: [Tag]
}
מילה מהירה על מיפוי מזהי מסמכים
לפני שנעבור למיפוי סוגים נוספים, בואו נדבר רגע על מיפוי מזהי מסמכים.
השתמשנו במעטפת המאפיין @DocumentID
בחלק מהדוגמאות הקודמות כדי למפות את מזהה המסמך של מסמכי Cloud Firestore שלנו למאפיין id
של סוגי Swift שלנו. זה חשוב מכמה סיבות:
- זה עוזר לנו לדעת איזה מסמך לעדכן במקרה שהמשתמש יבצע שינויים מקומיים.
-
List
של SwiftUI דורשת שהרכיבים שלה יהיוIdentifiable
כדי למנוע מאלמנטים לקפוץ מסביב כשהם מוכנסים.
כדאי לציין שתכונה המסומנת כ- @DocumentID
לא תקודד על ידי המקודד של Cloud Firestore בעת כתיבת המסמך בחזרה. הסיבה לכך היא שמזהה המסמך אינו תכונה של המסמך עצמו - כך שכתיבתו למסמך תהיה טעות.
בעבודה עם סוגים מקוננים (כגון מערך התגים Book
בדוגמה קודמת במדריך זה), אין צורך להוסיף מאפיין @DocumentID
: מאפיינים מקוננים הם חלק ממסמך Cloud Firestore, ואינם מהווים מסמך נפרד. לפיכך, הם לא צריכים מזהה מסמך.
תאריכים ושעות
ל-Cloud Firestore יש סוג נתונים מובנה לטיפול בתאריכים ושעות, ובזכות התמיכה של Cloud Firestore ב-Codable, קל להשתמש בהם.
בואו נסתכל על המסמך הזה שמייצג את האם של כל שפות התכנות, עדה, שהומצאה ב-1843:
סוג Swift למיפוי מסמך זה עשוי להיראות כך:
struct ProgrammingLanguage: Codable {
@DocumentID var id: String?
var name: String
var year: Date
}
אנחנו לא יכולים לעזוב את הקטע הזה על תאריכים ושעות בלי לנהל שיחה על @ServerTimestamp
. עטיפת נכסים זו היא מעצמה בכל הנוגע להתמודדות עם חותמות זמן באפליקציה שלך.
בכל מערכת מבוזרת, רוב הסיכויים שהשעונים במערכות הבודדות לא מסונכרנים לחלוטין כל הזמן. אולי תחשוב שזה לא עניין גדול, אבל תאר לעצמך את ההשלכות של שעון שיוצא מעט מסונכרן עבור מערכת סחר במניות: אפילו סטייה של אלפיות השנייה עשויה לגרום להפרש של מיליוני דולרים בעת ביצוע עסקה.
Cloud Firestore מטפל בתכונות המסומנות ב- @ServerTimestamp
באופן הבא: אם התכונה היא nil
כאשר אתה מאחסן אותה (באמצעות addDocument()
למשל), Cloud Firestore תאכל את השדה בחותמת הזמן הנוכחית של השרת בזמן כתיבתו למסד הנתונים . אם השדה אינו nil
כאשר אתה קורא addDocument()
או updateData()
, Cloud Firestore ישאיר את ערך התכונה ללא נגיעה. בדרך זו, קל ליישם שדות כמו createdAt
ו- lastUpdatedAt
.
נקודות גיאוגרפיות
מיקומים גיאוגרפיים נמצאים בכל מקום באפליקציות שלנו. תכונות מרגשות רבות מתאפשרות על ידי אחסוןן. לדוגמה, זה עשוי להיות שימושי לאחסן מיקום עבור משימה כדי שהאפליקציה שלך תוכל להזכיר לך על משימה כאשר אתה מגיע ליעד.
ל-Cloud Firestore יש סוג נתונים מובנה, GeoPoint
, שיכול לאחסן את קו האורך והרוחב של כל מיקום. כדי למפות מיקומים מ/אל מסמך Cloud Firestore, נוכל להשתמש בסוג GeoPoint
:
struct Office: Codable {
@DocumentID var id: String?
var name: String
var location: GeoPoint
}
הסוג המקביל ב-Swift הוא CLLocationCoordinate2D
, ונוכל למפות בין שני הסוגים הללו בפעולה הבאה:
CLLocationCoordinate2D(latitude: office.location.latitude,
longitude: office.location.longitude)
למידע נוסף על שאילתת מסמכים לפי מיקום פיזי, עיין במדריך הפתרונות הזה .
תקצירים
Enums הם כנראה אחת מתכונות השפה המוערכות ביותר בסוויפט; יש בהם הרבה יותר ממה שנראה לעין. מקרה שימוש נפוץ עבור enums הוא מודל של מצבים בדידים של משהו. לדוגמה, ייתכן שאנו כותבים אפליקציה לניהול מאמרים. כדי לעקוב אחר הסטטוס של מאמר, אולי נרצה להשתמש Status
enum :
enum Status: String, Codable {
case draft
case inReview
case approved
case published
}
Cloud Firestore לא תומכת ב-enums באופן מקורי (כלומר, היא לא יכולה לאכוף את קבוצת הערכים), אבל אנחנו עדיין יכולים להשתמש בעובדה שניתן להקליד enums ולבחור סוג שניתן לקידוד. בדוגמה זו, בחרנו String
, כלומר כל ערכי ה-enum ימופו אל/ממחרוזת כאשר הם מאוחסנים במסמך Cloud Firestore.
ומכיוון ש-Swift תומכת בערכים גולמיים מותאמים אישית, אנו יכולים אפילו להתאים אישית אילו ערכים מתייחסים לאיזה מקרה enum. כך, למשל, אם החלטנו לאחסן את המקרה Status.inReview
כ"בבדיקה", נוכל פשוט לעדכן את הרשימה לעיל באופן הבא:
enum Status: String, Codable {
case draft
case inReview = "in review"
case approved
case published
}
התאמה אישית של המיפוי
לפעמים, שמות המאפיינים של מסמכי Cloud Firestore שאנו רוצים למפות אינם תואמים לשמות המאפיינים במודל הנתונים שלנו ב-Swift. לדוגמה, אחד מעמיתינו לעבודה עשוי להיות מפתח Python, והחליט לבחור ב-snake_case עבור כל שמות התכונות שלהם.
אל תדאג: Codable מטפל בנו!
במקרים כאלה, נוכל להשתמש ב- CodingKeys
. זהו מנה שאנו יכולים להוסיף למבנה הניתן לקידוד כדי לציין כיצד תכונות מסוימות ימופו.
שקול את המסמך הזה:
כדי למפות מסמך זה למבנה בעל מאפיין שם מסוג String
, עלינו להוסיף Enum CodingKeys
למבנה ProgrammingLanguage
, ולציין את שם התכונה במסמך:
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
}
}
כברירת מחדל, ה-Codable API ישתמש בשמות המאפיינים של סוגי Swift שלנו כדי לקבוע את שמות התכונות במסמכי Cloud Firestore שאנו מנסים למפות. אז כל עוד שמות המאפיינים תואמים, אין צורך להוסיף CodingKeys
לסוגי הקידוד שלנו. עם זאת, ברגע שאנו משתמשים CodingKeys
עבור סוג מסוים, עלינו להוסיף את כל שמות הנכסים שאנו רוצים למפות.
בקטע הקוד למעלה, הגדרנו מאפיין id
שאולי נרצה להשתמש בו כמזהה בתצוגת List
SwiftUI. אם לא נציין את זה ב- CodingKeys
, זה לא היה ממופה בעת שליפת נתונים, ובכך הופך nil
. זה יביא לכך שתצוגת List
תתמלא במסמך הראשון.
כל מאפיין שאינו רשום כ-case ב- CodingKeys
המתאים יתעלם במהלך תהליך המיפוי. זה למעשה יכול להיות נוח אם אנחנו רוצים ספציפית לא לכלול חלק מהמאפיינים ממופה.
כך למשל, אם ברצוננו לא לכלול את המאפיין reasonWhyILoveThis
ממופה, כל שעלינו לעשות הוא להסיר אותו מ- CodingKeys
enum:
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
}
}
מדי פעם אולי נרצה לכתוב תכונה ריקה בחזרה למסמך Cloud Firestore. לסוויפט יש את הרעיון של אופציונליות לציון היעדר ערך, ו-Cloud Firestore תומך גם בערכים null
. עם זאת, התנהגות ברירת המחדל עבור קידוד אופציונליים בעלי ערך nil
היא פשוט להשמיט אותם. @ExplicitNull
נותן לנו שליטה מסוימת על אופן הטיפול באופציות של Swift בעת קידודן: על ידי סימון מאפיין אופציונלי כ- @ExplicitNull
, נוכל לומר ל-Cloud Firestore לכתוב מאפיין זה למסמך עם ערך null אם הוא מכיל ערך של nil
.
שימוש במקודד ומפענח מותאמים אישית למיפוי צבעים
כנושא אחרון בסיקור נתוני המיפוי שלנו עם Codable, בואו נציג מקודדים ומפענחים מותאמים אישית. סעיף זה אינו מכסה סוג נתונים מקורי של Cloud Firestore, אך מקודדים ומפענחים מותאמים אישית הם שימושיים רבים באפליקציות Cloud Firestore שלך.
"איך אני יכול למפות צבעים" היא אחת השאלות הנפוצות ביותר של מפתחים, לא רק עבור Cloud Firestore, אלא גם עבור מיפוי בין Swift ל-JSON. יש הרבה פתרונות שם בחוץ, אבל רובם מתמקדים ב-JSON, וכמעט כולם ממפים צבעים כמילון מקונן המורכב ממרכיבי ה-RGB שלו.
נראה שצריך להיות פתרון טוב ופשוט יותר. למה שלא נשתמש בצבעי אינטרנט (או, ליתר דיוק, סימון צבע משושה של CSS) - הם קלים לשימוש (בעצם רק מחרוזת), והם אפילו תומכים בשקיפות!
כדי להיות מסוגל למפות Swift Color
לערך הhex שלו, עלינו ליצור הרחבה של Swift שמוסיפה Codable ל- 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)
}
}
על ידי שימוש decoder.singleValueContainer()
, נוכל לפענח String
Color
המקבילה שלה, מבלי שנצטרך לקנן את רכיבי RGBA. בנוסף, אתה יכול להשתמש בערכים האלה בממשק האינטרנט של האפליקציה שלך, מבלי שתצטרך להמיר אותם תחילה!
בעזרת זה, אנו יכולים לעדכן קוד עבור תגי מיפוי, מה שמקל על הטיפול בצבעי התג ישירות במקום למפות אותם באופן ידני בקוד ממשק המשתמש של האפליקציה שלנו:
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]
}
טיפול בשגיאות
בקטעי הקוד שלמעלה שמרנו בכוונה על טיפול בשגיאות למינימום, אבל באפליקציית ייצור, תרצה להקפיד לטפל בחן בכל שגיאה.
להלן קטע קוד שמראה כיצד להשתמש בכל מצבי שגיאה שאתה עלול להיתקל בהם:
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)"
}
}
}
}
}
טיפול בשגיאות בעדכונים חיים
קטע הקוד הקודם מדגים כיצד לטפל בשגיאות בעת שליפת מסמך בודד. בנוסף לאיסוף נתונים פעם אחת, Cloud Firestore תומכת גם באספקת עדכונים לאפליקציה שלך בזמן שהם מתרחשים, באמצעות מה שנקרא מאזיני תמונת מצב: אנו יכולים לרשום מאזין תמונת מצב על אוסף (או שאילתה), ו-Cloud Firestore תתקשר למאזין שלנו בכל פעם שיש הוא עדכון.
להלן קטע קוד שמראה כיצד לרשום מאזין תמונת מצב, למפות נתונים באמצעות Codable ולטפל בכל שגיאה שעלולה להתרחש. זה גם מראה כיצד להוסיף מסמך חדש לאוסף. כפי שתראו, אין צורך לעדכן את המערך המקומי המחזיק את המסמכים הממופים בעצמנו, שכן הדבר מטופל על ידי הקוד במאזין ה-Snapshot.
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)
}
}
}
כל קטעי הקוד שבהם נעשה שימוש בפוסט זה הם חלק מיישום לדוגמה שתוכל להוריד ממאגר GitHub זה .
צא והשתמש ב-Codable!
ה-Codable API של Swift מספק דרך רבת עוצמה וגמישה למפות נתונים מפורמטים מסודרים אל וממודל הנתונים של היישומים שלך. במדריך זה ראית כמה קל להשתמש באפליקציות שמשתמשות ב-Cloud Firestore כחנות הנתונים שלהן.
החל מדוגמה בסיסית עם סוגי נתונים פשוטים, הגדלנו בהדרגה את המורכבות של מודל הנתונים, תוך כדי יכולת להסתמך על ההטמעה של Codable ו-Firebase כדי לבצע עבורנו את המיפוי.
לפרטים נוספים על Codable, אני ממליץ על המשאבים הבאים:
- לג'ון סנדל יש מאמר נחמד על היסודות של Codable .
- אם ספרים הם יותר הקטע שלך, בדוק את מדריך בית הספר לטיסה של Matt to Swift Codable .
- ולבסוף, לדוני וולס יש סדרה שלמה על Codable .
למרות שעשינו כמיטב יכולתנו להרכיב מדריך מקיף למיפוי מסמכי Cloud Firestore, זה לא ממצה, וייתכן שאתה משתמש באסטרטגיות אחרות למיפוי הסוגים שלך. באמצעות לחצן שלח משוב למטה, ספר לנו באילו אסטרטגיות אתה משתמש למיפוי סוגים אחרים של נתונים של Cloud Firestore או ייצוג נתונים ב-Swift.
אין באמת סיבה לא להשתמש בתמיכה ב-Codable של Cloud Firestore.