نگاشت داده‌های Cloud Firestore با Swift Codable

Swift Codable API که در Swift 4 معرفی شد، به ما امکان می‌دهد از قدرت کامپایلر برای تسهیل نگاشت داده‌ها از قالب‌های سریالی به انواع Swift استفاده کنیم.

ممکن است از Codable برای نگاشت داده‌ها از میانای برنامه‌سازی کاربردی وب به مدل داده برنامه‌تان (و برعکس) استفاده کرده باشید، اما Codable بسیار انعطاف‌پذیرتر از این است.

در این راهنما، نحوه استفاده از Codable را برای نگاشت داده‌ها از Cloud Firestore به انواع Swift و بالعکس بررسی خواهیم کرد.

هنگام واکشی سند از Cloud Firestore، برنامه‌تان واژه‌نامه‌ای از جفت‌های کلید/مقدار (یا آرایه‌ای از واژه‌نامه‌ها، اگر از یکی از عملیات‌های برگرداننده چند سند استفاده کنید) دریافت خواهد کرد.

اکنون، قطعاً می‌توانید به استفاده مستقیم از واژه‌نامه‌ها در Swift ادامه دهید و آن‌ها انعطاف‌پذیری فوق‌العاده‌ای را ارائه می‌دهند که ممکن است دقیقاً همان چیزی باشد که مورد استفاده شما نیاز دارد. However, this approach isn't type safe and it's easy to introduce hard-to-track-down bugs by misspelling attribute names, or forgetting to map the new attribute your team added when they shipped that exciting new feature last week.

در گذشته، بسیاری از توسعه‌دهندگان با پیاده‌سازی یک لایه نگاشت ساده که به آن‌ها اجازه می‌داد واژه‌نامه‌ها را به انواع 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. مطمئن شوید چارچوب 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 از تعدادی انواع داده دیگر پشتیبانی می‌کند که برخی‌از آن‌ها انواع ساختاریافته‌ای هستند که می‌توانید از آن‌ها برای ایجاد اشیای تودرتو در سند استفاده کنید.

انواع سفارشی تودرتو

بیشتر مشخصه‌هایی که می‌خواهیم در اسنادمان نگاشت کنیم مقادیر ساده‌ای هستند، مثل عنوان کتاب یا نام نویسنده. اما در مواردی که لازم است شیء پیچیده‌تری را ذخیره کنیم چه؟ برای مثال، ممکن است بخواهیم نشانی‌های وب جلد کتاب را با وضوح‌های مختلف ذخیره کنیم.

ساده‌ترین راه برای انجام این کار در Cloud Firestore استفاده از نقشه است:

ذخیره کردن نوع سفارشی تودرتو در سند Firestore

هنگام نوشتن ساختار Swift مربوطه، می‌توانیم از این واقعیت استفاده کنیم که Cloud Firestore از نشانی‌های وب پشتیبانی می‌کند — هنگام ذخیره کردن فیلدی که حاوی نشانی وب است، به رشته تبدیل می‌شود و برعکس:

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]
}

توضیح مختصری درباره نگاشت شناسه‌های سند

قبل‌از اینکه به نگاشت انواع بیشتری بپردازیم، اجازه دهید کمی درباره نگاشت شناسه‌های سند صحبت کنیم.

در برخی‌از مثال‌های قبلی، از پوشاننده ویژگی @DocumentID استفاده کردیم تا شناسه سند اسناد Cloud Firestore را به ویژگی id انواع Swift نگاشت کنیم. این موضوع به چند دلیل مهم است:

  • این کار به ما کمک می‌کند بدانیم درصورت اعمال تغییرات محلی توسط کاربر، کدام سند را به‌روز کنیم.
  • List SwiftUI به عناصر خود نیاز دارد تا Identifiable باشند تا از پریدن عناصر به اطراف هنگام درج شدن جلوگیری شود.

شایان ذکر است که مشخصه‌ای که به‌عنوان @DocumentID علامت‌گذاری شده است، هنگام بازنویسی سند توسط کدبند Cloud Firestore کدبندی نخواهد شد. دلیلش این است که شناسه سند جزو مشخصه‌های خود سند نیست — بنابراین نوشتن آن در سند اشتباه است.

When working with nested types (such as the array of tags on the Book in an earlier example in this guide), it is not required to add a @DocumentID property: nested properties are a part of the Cloud Firestore document, and do not constitute a separate document. بنابراین، به شناسه سند نیاز ندارند.

تاریخ و زمان

Cloud Firestore نوع داده‌ای داخلی برای مدیریت تاریخ و زمان دارد و به‌لطف پشتیبانی Cloud Firestore از Codable، استفاده از آن‌ها بسیار ساده است.

بیایید نگاهی به این سند بیندازیم که نشان‌دهنده مادر همه زبان‌های برنامه‌نویسی، یعنی «آدا»، است که در سال ۱۸۴۳ اختراع شد:

ذخیره کردن تاریخ‌ها در سند Firestore

نوع Swift برای نگاشت این سند ممکن است به‌این‌صورت باشد:

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

نمی‌توانیم این بخش مربوط به تاریخ و زمان را بدون صحبت کردن درباره @ServerTimestamp ترک کنیم. این پوشاننده ویژگی هنگام سروکار داشتن با مُهرهای زمان در برنامه‌تان، بسیار قدرتمند عمل می‌کند.

در هر سیستم توزیع‌شده‌ای، احتمال دارد که ساعت‌های سیستم‌های مجزا همیشه کاملاً همگام‌سازی نشده باشند. شاید فکر کنید این موضوع چندان مهم نیست، اما پیامدهای ناهماهنگی جزئی ساعت را برای سیستم معامله سهام تصور کنید: حتی یک میلی‌ثانیه انحراف ممکن است منجر به تفاوت میلیون‌ها دلار در اجرای معامله شود.

Cloud Firestore مشخصه‌های علامت‌گذاری‌شده با @ServerTimestamp را به‌صورت زیر مدیریت می‌کند: اگر مشخصه هنگام ذخیره کردن آن (مثلاً بااستفاده از addDocument()) nil باشد، Cloud Firestore فیلد را با مُهر زمان فعلی سرور در زمان نوشتن آن در پایگاه داده پر می‌کند. اگر هنگام فراخوانی addDocument() یا updateData()، فیلد nil نباشد، 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 یکی از ویژگی‌های زبانی است که در 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 با نام مشخصه snake_cased

برای نگاشت این سند به ساختاری که ویژگی نام از نوع String دارد، باید یک شمارشی 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 با اولین سند پر شود.

هر دارایی‌ای که به‌عنوان مورد در CodingKeys enum مربوطه فهرست نشده باشد درطول فرایند نگاشت نادیده گرفته خواهد شد. این کار می‌تواند بسیار راحت باشد اگر به‌طور خاص بخواهیم برخی‌از ویژگی‌ها را از نگاشت شدن مستثنی کنیم.

بنابراین برای مثال، اگر بخواهیم دارایی reasonWhyILoveThis را از نگاشت شدن مستثنی کنیم، تنها کاری که باید انجام دهیم این است که آن را از شمارش 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 gives us some control over how Swift optionals are handled when encoding them: by flagging an optional property as @ExplicitNull, we can tell Cloud Firestore to write this property to the document with a null value if it contains a value of nil.

استفاده از رمزگذار و رمزگشای سفارشی برای نگاشت رنگ‌ها

به‌عنوان آخرین موضوع در پوشش ما از نگاشت داده‌ها با Codable، بیایید رمزگذارها و رمزگشاهای سفارشی را معرفی کنیم. این بخش نوع داده Cloud Firestore بومی را پوشش نمی‌دهد، اما رمزگذارها و رمزگشاهای سفارشی در برنامه‌های Cloud Firestore شما بسیار مفید هستند.

«چگونه می‌توانم رنگ‌ها را نگاشت کنم» یکی از متداول‌ترین سؤالات توسعه‌دهندگان است، نه فقط برای Cloud Firestore، بلکه برای نگاشت بین Swift و JSON نیز هست. راه‌حل‌های زیادی وجود دارد، اما اکثر آن‌ها بر JSON تمرکز دارند و تقریباً همه آن‌ها رنگ‌ها را به‌عنوان یک فرهنگ لغت تودرتو متشکل از مؤلفه‌های RGB آن نگاشت می‌کنند.

به‌نظر می‌رسد باید راه‌حل بهتر و ساده‌تری وجود داشته باشد. چرا از رنگ‌های وب استفاده نمی‌کنیم (یا به‌طور دقیق‌تر، نمادگذاری رنگ hex در CSS) — استفاده از آن‌ها آسان است (اساساً فقط یک رشته است)، و حتی از شفافیت هم پشتیبانی می‌کنند!

برای اینکه بتوانیم Swift Color را به مقدار هگزادسیمال آن نگاشت کنیم، باید افزونه 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، و رسیدگی به هرگونه خطایی را که ممکن است رخ دهد نشان می‌دهد. همچنین نحوه افزودن سند جدید به مجموعه را نشان می‌دهد. As you will see, there is no need to update the local array holding the mapped documents ourselves, as this is taken care of by the code in the snapshot listener.

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 استفاده می‌کنید.

واقعاً هیچ دلیلی برای عدم استفاده از پشتیبانی Codable Cloud Firestore وجود ندارد.