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

‫Codable API של Swift, שהוצג ב-Swift 4, מאפשר לנו להשתמש ביכולות של הקומפיילר כדי למפות בקלות רבה יותר נתונים מפורמטים שעברו סריאליזציה לסוגים של Swift.

יכול להיות שהשתמשתם ב-Codable כדי למפות נתונים מ-Web 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?

לפי התיעוד של אפל, 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. כשאנחנו מסמנים את המאפיין cover ב-BookWithCoverImages כאופציונלי, אנחנו יכולים להתמודד עם העובדה שבמסמכים מסוימים אין מאפיין cover.

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

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

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

השתמשנו ב-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. המעטפת הזו של המאפיין היא כלי רב-עוצמה לטיפול בחותמות זמן באפליקציה.

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

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

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

כדי למפות את המסמך הזה ל-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
  }
}

כברירת מחדל, Codable API ישתמש בשמות המאפיינים של סוגי ה-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.