نقشه داده های Cloud Firestore با Swift Codable

API قابل کدگذاری سوئیفت که در سوئیفت ۴ معرفی شد، ما را قادر می‌سازد تا از قدرت کامپایلر برای آسان‌تر کردن نگاشت داده‌ها از قالب‌های سریالی به انواع سوئیفت استفاده کنیم.

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

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

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

اکنون، مطمئناً می‌توانید به استفاده مستقیم از دیکشنری‌ها در سوئیفت ادامه دهید، و آنها انعطاف‌پذیری بسیار خوبی را ارائه می‌دهند که ممکن است دقیقاً همان چیزی باشد که مورد استفاده شما نیاز دارد. با این حال، این رویکرد از نظر نوع داده ایمن نیست و به راحتی می‌توان با اشتباه املایی نام ویژگی‌ها، یا فراموش کردن نگاشت ویژگی جدیدی که تیم شما هنگام ارائه آن ویژگی جدید هیجان‌انگیز هفته گذشته اضافه کرده است، اشکالاتی را ایجاد کرد که ردیابی آنها دشوار است.

در گذشته، بسیاری از توسعه‌دهندگان با پیاده‌سازی یک لایه نگاشت ساده که به آنها اجازه می‌داد دیکشنری‌ها را به انواع Swift نگاشت کنند، بر این کاستی‌ها غلبه می‌کردند. اما باز هم، اکثر این پیاده‌سازی‌ها مبتنی بر تعیین دستی نگاشت بین اسناد Cloud Firestore و انواع متناظر مدل داده برنامه شما هستند.

با پشتیبانی Cloud Firestore از API قابل کدگذاری Swift، این کار بسیار آسان‌تر می‌شود:

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

داده‌های نقشه‌برداری

Cloud Firestore داده‌ها را در اسنادی ذخیره می‌کند که کلیدها را به مقادیر نگاشت می‌کنند. برای واکشی داده‌ها از یک سند واحد، می‌توانیم DocumentSnapshot.data() را فراخوانی کنیم، که یک دیکشنری را برمی‌گرداند که نام فیلدها را به Any : func data() -> [String : Any]? نگاشت می‌کند.

این یعنی می‌توانیم از سینتکس زیرنویس سوئیفت برای دسترسی به هر فیلد به صورت جداگانه استفاده کنیم.

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. مطمئن شوید که فریم‌ورک FirebaseFirestore را به پروژه خود اضافه کرده‌اید. برای این کار می‌توانید از Swift Package Manager یا CocoaPods استفاده کنید.
  2. FirebaseFirestore به فایل Swift خود وارد کنید.
  3. نوع خود را با Codable مطابقت دهید.
  4. (اختیاری، اگر می‌خواهید از نوع در نمای List استفاده کنید) یک ویژگی id به نوع خود اضافه کنید و با استفاده از @DocumentID به Cloud Firestore بگویید که این را به شناسه سند نگاشت کند. در ادامه با جزئیات بیشتری در مورد این موضوع صحبت خواهیم کرد.
  5. برای نگاشت یک ارجاع سند به یک نوع Swift documentReference.data(as: ) استفاده کنید.
  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]
}

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

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

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

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

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

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

تاریخ‌ها و زمان‌ها

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

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

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

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

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

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

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

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

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

انوم‌ها

Enumها احتمالاً یکی از دست‌کم‌گرفته‌شده‌ترین ویژگی‌های زبان برنامه‌نویسی در سوئیفت هستند؛ قابلیت‌های آن‌ها بسیار بیشتر از آن چیزی است که به چشم می‌آید. یک مورد استفاده رایج برای enumها، مدل‌سازی حالت‌های گسسته چیزی است. برای مثال، ممکن است در حال نوشتن برنامه‌ای برای مدیریت مقالات باشیم. برای پیگیری وضعیت یک مقاله، ممکن است بخواهیم از یک enum Status استفاده کنیم:

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

Cloud Firestore به طور بومی از enumها پشتیبانی نمی‌کند (یعنی نمی‌تواند مجموعه مقادیر را اعمال کند)، اما ما همچنان می‌توانیم از این واقعیت که enumها می‌توانند تایپ شوند استفاده کنیم و یک نوع قابل کدگذاری را انتخاب کنیم. در این مثال، ما String انتخاب کرده‌ایم، به این معنی که تمام مقادیر enum هنگام ذخیره شدن در یک سند Cloud Firestore به/از رشته نگاشت می‌شوند.

و از آنجایی که سوئیفت از مقادیر خام سفارشی پشتیبانی می‌کند، می‌توانیم حتی سفارشی کنیم که کدام مقادیر به کدام حالت شمارشی اشاره می‌کنند. بنابراین، برای مثال، اگر تصمیم بگیریم حالت Status.inReview را به صورت "در حال بررسی" ذخیره کنیم، می‌توانیم enum فوق را به صورت زیر به‌روزرسانی کنیم:

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

سفارشی‌سازی نقشه‌برداری

گاهی اوقات، نام ویژگی‌های اسناد Cloud Firestore که می‌خواهیم نگاشت کنیم با نام ویژگی‌های موجود در مدل داده ما در Swift مطابقت ندارد. برای مثال، یکی از همکاران ما ممکن است توسعه‌دهنده پایتون باشد و تصمیم گرفته باشد که برای همه نام ویژگی‌های خود snake_case را انتخاب کند.

نگران نباشید: Codable ما را پوشش می‌دهد!

برای مواردی از این دست، می‌توانیم از CodingKeys استفاده کنیم. این یک enum است که می‌توانیم به یک ساختار قابل کدگذاری اضافه کنیم تا نحوه نگاشت ویژگی‌های خاص را مشخص کنیم.

این سند را در نظر بگیرید:

یک سند Firestore با ویژگی snake_cased

برای نگاشت این سند به ساختاری که دارای ویژگی name از نوع 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
  }
}

به طور پیش‌فرض، API کدپذیر (Codable API) از نام‌های ویژگی انواع Swift ما برای تعیین نام‌های ویژگی در اسناد Cloud Firestore که می‌خواهیم نگاشت کنیم، استفاده می‌کند. بنابراین تا زمانی که نام‌های ویژگی مطابقت داشته باشند، نیازی به اضافه کردن CodingKeys به انواع کدپذیر ما نیست. با این حال، هنگامی که CodingKeys برای یک نوع خاص استفاده می‌کنیم، باید نام‌های تمام ویژگی‌هایی را که می‌خواهیم نگاشت کنیم، اضافه کنیم.

در قطعه کد بالا، یک ویژگی id تعریف کرده‌ایم که ممکن است بخواهیم از آن به عنوان شناسه در نمای List SwiftUI استفاده کنیم. اگر آن را در CodingKeys مشخص نکنیم، هنگام واکشی داده‌ها نگاشت نمی‌شود و بنابراین nil می‌شود. این امر منجر به پر شدن نمای List با اولین سند می‌شود.

هر ویژگی که به عنوان یک مورد (case) در 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 بنویسیم. سوئیفت مفهوم optionals را برای نشان دادن عدم وجود یک مقدار دارد و Cloud Firestore نیز از مقادیر null پشتیبانی می‌کند. با این حال، رفتار پیش‌فرض برای رمزگذاری optionals که مقدار nil دارند، حذف آنهاست. @ExplicitNull به ما امکان کنترل نحوه مدیریت optionals سوئیفت هنگام رمزگذاری آنها را می‌دهد: با علامت‌گذاری یک ویژگی اختیاری به عنوان @ExplicitNull ، می‌توانیم به Cloud Firestore بگوییم که اگر سند حاوی مقدار nil است، این ویژگی را با مقدار null بنویسد.

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

به عنوان آخرین موضوع در پوشش ما از نگاشت داده‌ها با 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 همچنین از ارائه به‌روزرسانی‌ها به برنامه شما در هنگام وقوع، با استفاده از شنونده‌های snapshot پشتیبانی می‌کند: می‌توانیم یک شنونده snapshot را روی یک مجموعه (یا پرس‌وجو) ثبت کنیم و Cloud Firestore هر زمان که به‌روزرسانی وجود داشته باشد، شنونده ما را فراخوانی می‌کند.

در اینجا قطعه کدی وجود دارد که نحوه ثبت یک شنونده snapshot، نگاشت داده‌ها با استفاده از 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)
    }
  }
}

تمام قطعه کدهای استفاده شده در این پست بخشی از یک برنامه نمونه هستند که می‌توانید از این مخزن گیت‌هاب دانلود کنید.

برو جلو و از Codable استفاده کن!

API Codable سوئیفت روشی قدرتمند و انعطاف‌پذیر برای نگاشت داده‌ها از قالب‌های سریالی به/از مدل داده برنامه‌های شما ارائه می‌دهد. در این راهنما، دیدید که استفاده از آن در برنامه‌هایی که از Cloud Firestore به عنوان مخزن داده خود استفاده می‌کنند، چقدر آسان است.

با شروع از یک مثال پایه با انواع داده‌های ساده، ما به تدریج پیچیدگی مدل داده را افزایش دادیم، در عین حال که می‌توانستیم برای انجام نگاشت به پیاده‌سازی Codable و Firebase تکیه کنیم.

برای جزئیات بیشتر در مورد Codable، منابع زیر را توصیه می‌کنم:

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

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