מיפוי נתונים של Cloud Firestore באמצעות Swift Codable

‫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, מה שיגרום לבעיית מיפוי שקשה למצוא. בנוסף, תצטרכו לעדכן את קוד המיפוי בכל פעם שיתווסף שדה חדש, וזה תהליך די מסורבל.

בנוסף, אנחנו לא מנצלים את מערכת הטיפוסים החזקה של Swift, שיודעת בדיוק מהו הטיפוס הנכון לכל אחד מהמאפיינים של Book.

מה זה בכלל Codable?

לפי התיעוד של Apple, ‏Codable הוא "סוג שיכול להמיר את עצמו לייצוג חיצוני וממנו". למעשה, Codable הוא כינוי לסוג של הפרוטוקולים Encodable ו-Decodable. כשמתאימים סוג 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 תומך במגוון רחב של סוגי נתונים, ממחרוזות פשוטות ועד למפות בתצוגת עץ. רוב הסוגים האלה תואמים ישירות לסוגים המובנים של Swift. לפני שנתחיל לעבוד עם סוגי נתונים מורכבים, בואו נראה איך ממפים כמה סוגי נתונים פשוטים.

כדי למפות מסמכי Cloud Firestore לסוגי Swift, פועלים לפי השלבים הבאים:

  1. חשוב לוודא שהוספת את ה-framework של FirebaseFirestore לפרויקט. אפשר להשתמש ב-Swift Package Manager או ב-CocoaPods כדי לעשות את זה.
  2. מייבאים את FirebaseFirestore לקובץ Swift.
  3. התאמת הסוג ל-Codable.
  4. (אופציונלי, אם רוצים להשתמש בסוג בתצוגת List) מוסיפים מאפיין id לסוג, ומשתמשים ב-@DocumentID כדי להורות ל-Cloud Firestore למפות אותו למזהה המסמך. בהמשך נרחיב בנושא הזה.
  5. משתמשים ב-documentReference.data(as: ) כדי למפות הפניה למסמך לסוג Swift.
  6. משתמשים ב-documentReference.setData(from: ) כדי למפות נתונים מסוגי Swift למסמך Cloud Firestore.
  7. (אופציונלי, אבל מומלץ מאוד) מטמיעים טיפול תקין בשגיאות.

נעדכן את הסוג 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 תומך בכמה סוגי נתונים אחרים, שחלקם הם סוגים מובְנים שבהם אפשר להשתמש כדי ליצור אובייקטים בתצוגת עץ בתוך מסמך.

סוגים מותאמים אישית שמוצבים בתוך סוגים אחרים

רוב המאפיינים שאנחנו רוצים למפות במסמכים הם ערכים פשוטים, כמו שם הספר או שם המחבר. אבל מה אם אנחנו צריכים לאחסן אובייקט מורכב יותר? לדוגמה, יכול להיות שנרצה לאחסן את כתובות ה-URL של כריכת הספר ברזולוציות שונות.

הדרך הכי קלה לעשות את זה ב-Cloud Firestore היא להשתמש במפה:

אחסון סוג מותאם אישית בתצוגת עץ במסמך 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 או אליו: כל זה עובד עם הקוד שכתבנו בחלק הראשון.

מערכים

לפעמים אנחנו רוצים לאחסן אוסף של ערכים במסמך. הז'אנרים של ספר הם דוגמה טובה: ספר כמו מדריך הטרמפיסט לגלקסיה יכול להשתייך לכמה קטגוריות – במקרה הזה, "מדע בדיוני" ו "קומדיה":

אחסון מערך במסמך 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]
}

מכיוון שזה עובד עם כל סוג שניתן לקידוד, אנחנו יכולים להשתמש גם בסוגים מותאמים אישית. נניח שאנחנו רוצים לאחסן רשימה של תגים לכל ספר. בנוסף לשם התג, אנחנו רוצים לשמור גם את הצבע של התג, כך:

אחסון מערך של סוגים מותאמים אישית במסמך Firestore

כדי לאחסן תגים בדרך הזו, כל מה שצריך לעשות הוא להטמיע מבנה 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]
}

כמה מילים על מיפוי מזהי מסמכים

לפני שנמשיך למפות סוגים נוספים, בואו נדבר לרגע על מיפוי מזהי מסמכים.

השתמשנו ב-property wrapper‏ @DocumentID בחלק מהדוגמאות הקודמות כדי למפות את מזהה המסמך של מסמכי Cloud Firestore למאפיין id של סוגי Swift. זה חשוב מכמה סיבות:

  • הוא עוזר לנו לדעת איזה מסמך לעדכן אם המשתמש מבצע שינויים מקומיים.
  • כדי למנוע מהרכיבים לקפוץ כשמוסיפים אותם, הרכיבים של List ב-SwiftUI צריכים להיות Identifiable.

חשוב לציין שמאפיין שמסומן כ-@DocumentID לא יקודד על ידי המקודד של Cloud Firestore כשכותבים את המסמך בחזרה. הסיבה לכך היא שמזהה המסמך הוא לא מאפיין של המסמך עצמו, ולכן כתיבה שלו במסמך תהיה שגיאה.

כשעובדים עם סוגים בתצוגת עץ (כמו מערך התגים ב-Book בדוגמה הקודמת במדריך הזה), לא צריך להוסיף מאפיין @DocumentID: מאפיינים בתצוגת עץ הם חלק מהמסמך Cloud Firestore, והם לא מהווים מסמך נפרד. לכן, לא צריך מזהה מסמך בשבילן.

תאריכים ושעות

ב-Cloud Firestore יש סוג נתונים מובנה לטיפול בתאריכים ובשעות, ובזכות התמיכה של Cloud Firestore ב-Codable, קל להשתמש בהם.

בואו נסתכל על המסמך הזה שמייצג את האם של כל שפות התכנות, Ada, שהומצאה ב-1843:

אחסון תאריכים במסמך Firestore

סוג Swift למיפוי המסמך הזה עשוי להיראות כך:

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

אנחנו לא יכולים לסיים את החלק הזה על תאריכים ושעות בלי לדבר על @ServerTimestamp. ה-property wrapper הזה הוא כלי רב עוצמה כשמדובר בטיפול בחותמות זמן באפליקציה.

בכל מערכת מבוזרת, סביר להניח שהשעונים במערכות הנפרדות לא מסונכרנים באופן מלא כל הזמן. אולי זה נראה לכם לא משמעותי, אבל תחשבו על ההשלכות של שעון שלא מסונכרן במערכת למסחר במניות: אפילו סטייה של אלפית השנייה יכולה לגרום להבדל של מיליוני דולרים בביצוע עסקה.

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

במדריך הזה מוסבר איך לשלוח שאילתות על מסמכים לפי מיקום פיזי.

טיפוסים בני מנייה (enum)

סוגי Enum הם כנראה אחת התכונות הכי פחות מוערכות בשפת Swift, אבל יש בהם הרבה יותר ממה שנראה לעין. תרחיש שימוש נפוץ ב-enums הוא ליצור מודל של המצבים הנפרדים של משהו. לדוגמה, נניח שאנחנו כותבים אפליקציה לניהול מאמרים. כדי לעקוב אחרי הסטטוס של מאמר, אפשר להשתמש ב-enum Status:

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

‫Cloud Firestore לא תומך בערכי enum באופן מקורי (כלומר, הוא לא יכול לאכוף את קבוצת הערכים), אבל אנחנו עדיין יכולים לנצל את העובדה שאפשר להקליד ערכי enum, ולבחור סוג שניתן לתכנות. בדוגמה הזו בחרנו באפשרות String, כלומר כל ערכי ה-enum ימופו למחרוזת או ממנה כשהם מאוחסנים במסמך Cloud Firestore.

ומכיוון ש-Swift תומכת בערכים גולמיים מותאמים אישית, אנחנו יכולים אפילו להתאים אישית אילו ערכים מתייחסים לאיזה מקרה enum. לדוגמה, אם החלטנו לשמור את המקרה Status.inReview כ-"בבדיקה", נוכל פשוט לעדכן את ה-enum שלמעלה באופן הבא:

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

התאמה אישית של המיפוי

לפעמים, שמות המאפיינים של מסמכי Cloud Firestore שאנחנו רוצים למפות לא תואמים לשמות המאפיינים במודל הנתונים שלנו ב-Swift. לדוגמה, אחד מהקולגות שלנו יכול להיות מפתח Python, והוא החליט לבחור ב-snake_case לכל שמות המאפיינים שלו.

אל דאגה: Codable דואג לנו!

במקרים כאלה, אנחנו יכולים להשתמש ב-CodingKeys. זהו enum שאנחנו יכולים להוסיף למבנה שניתן לקידוד כדי לציין איך מאפיינים מסוימים ימופו.

לפי המסמך הזה:

מסמך ב-Firestore עם שם מאפיין בפורמט snake_case

כדי למפות את המסמך הזה ל-struct שיש לו מאפיין שם מסוג String, צריך להוסיף טיפוס מנייה (enum) של CodingKeys ל-struct 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
  }
}

כברירת מחדל, ה-API של Codable ישתמש בשמות המאפיינים של סוגי ה-Swift שלנו כדי לקבוע את שמות המאפיינים במסמכי Cloud Firestore שאנחנו מנסים למפות. כל עוד שמות המאפיינים תואמים, אין צורך להוסיף את CodingKeys לסוגים שניתן לקודד. עם זאת, אחרי שמשתמשים ב-CodingKeys לסוג ספציפי, צריך להוסיף את כל שמות המאפיינים שרוצים למפות.

בקטע הקוד שלמעלה, הגדרנו מאפיין id שאולי נרצה להשתמש בו כמזהה בתצוגת List של SwiftUI. אם לא היינו מציינים אותו ב-CodingKeys, הוא לא היה ממופה בזמן אחזור הנתונים, ולכן היה הופך ל-nil. כתוצאה מכך, התצוגה של List תתמלא במסמך הראשון.

כל מאפיין שלא מופיע כמקרה ב-enum המתאים של CodingKeys לא יילקח בחשבון בתהליך המיפוי. זה יכול להיות נוח אם אנחנו רוצים להחריג חלק מהנכסים מהמיפוי.

לדוגמה, אם אנחנו רוצים להחריג את המאפיין reasonWhyILoveThis מהמיפוי, כל מה שצריך לעשות הוא להסיר אותו מה-enum של 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
  }
}

לפעמים אנחנו רוצים לכתוב מאפיין ריק בחזרה במסמך Cloud Firestore. ב-Swift יש מושג של אופציונליות כדי לציין היעדר ערך, ו-Cloud Firestore תומך גם בערכי null. עם זאת, פעולת ברירת המחדל לקידוד אופציונליים עם ערך nil היא פשוט להשמיט אותם. ‫@ExplicitNull נותן לנו שליטה מסוימת על האופן שבו מטפלים באופציונליים של Swift כשמקודדים אותם: על ידי סימון מאפיין אופציונלי כ-@ExplicitNull, אנחנו יכולים להורות ל-Cloud Firestore לכתוב את המאפיין הזה במסמך עם ערך null אם הוא מכיל ערך של nil.

שימוש במקודד ובמפענח בהתאמה אישית למיפוי צבעים

הנושא האחרון בסקירה שלנו על מיפוי נתונים באמצעות Codable הוא מקודדים ומפענחים בהתאמה אישית. בקטע הזה לא נסביר על סוג נתונים מקורי של Cloud Firestore, אבל מקודדים ומפענחים בהתאמה אישית הם שימושיים מאוד באפליקציות Cloud Firestore.

"איך ממפים צבעים" היא אחת מהשאלות הנפוצות ביותר של מפתחים, לא רק לגבי Cloud Firestore, אלא גם לגבי מיפוי בין Swift ל-JSON. יש הרבה פתרונות, אבל רובם מתמקדים ב-JSON, וכמעט כולם ממפים צבעים כמילון בתצוגת עץ שמורכב מרכיבי ה-RGB שלו.

נראה שצריך להיות פתרון טוב ופשוט יותר. למה לא להשתמש בצבעי אינטרנט (או ליתר דיוק, בסימון קוד צבע הקסדצימלי ב-CSS)? הם קלים לשימוש (בעצם רק מחרוזת) ואפילו תומכים בשקיפות.

כדי למפות את Color של Swift לערך ההקסדצימלי שלו, צריך ליצור תוסף של 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 ולטפל בשגיאות שעלולות להתרחש. בנוסף, מוסבר איך להוסיף מסמך חדש לאוסף. כמו שאפשר לראות, אין צורך לעדכן בעצמנו את המערך המקומי שמכיל את המסמכים הממופים, כי הקוד במאזין של תמונת המצב מטפל בזה.

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, מומלץ לעיין במקורות המידע הבאים:

למרות שעשינו כמיטב יכולתנו כדי להרכיב מדריך מקיף למיפוי מסמכי Cloud Firestore, הוא לא ממצה, ויכול להיות שאתם משתמשים באסטרטגיות אחרות כדי למפות את הסוגים שלכם. נשמח לקבל מכם משוב על האסטרטגיות שבהן אתם משתמשים כדי למפות סוגים אחרים של נתוני Cloud Firestore או לייצג נתונים ב-Swift. אתם יכולים ללחוץ על הכפתור שליחת משוב שבהמשך.

אין סיבה לא להשתמש בתמיכה של Cloud Firestore ב-Codable.