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

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

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

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

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

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

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

با پشتیبانی Cloud Firestore از Codable API سوئیفت، این کار بسیار آسان‌تر می‌شود:

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

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

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

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

import FirebaseFirestore

#warning("DO NOT MAP YOUR DOCUMENTS MANUALLY. USE CODABLE INSTEAD.")
func fetchBook(documentId: String) {
  let docRef = db.collection("books").document(documentId)

  docRef.getDocument { document, error in
    if let error = error as NSError? {
      self.errorMessage = "Error getting document: \(error.localizedDescription)"
    }
    else {
      if let document = document {
        let id = document.documentID
        let data = document.data()
        let title = data?["title"] as? String ?? ""
        let numberOfPages = data?["numberOfPages"] as? Int ?? 0
        let author = data?["author"] as? String ?? ""
        self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author)
      }
    }
  }
}

اگرچه ممکن است پیاده سازی آن ساده و آسان به نظر برسد، اما این کد شکننده است، نگهداری آن سخت و مستعد خطا است.

همانطور که می بینید، ما در مورد انواع داده های فیلدهای سند فرضیاتی ایجاد می کنیم. اینها ممکن است درست باشند یا نباشند.

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

و فراموش نکنیم که ما از سیستم نوع قوی سوئیفت که دقیقا نوع صحیح هر یک از ویژگی های Book را می شناسد، استفاده نمی کنیم.

به هر حال کدپذیر چیست؟

طبق مستندات اپل، Codable "نوعی است که می تواند خود را به یک نمایش خارجی تبدیل کند و از آن خارج شود." در واقع، Codable یک نوع مستعار برای پروتکل های Encodable و Decodable است. با تطبیق یک نوع سوئیفت با این پروتکل، کامپایلر کد مورد نیاز برای رمزگذاری/رمزگشایی یک نمونه از این نوع را از یک فرمت سریالی، مانند 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
با استفاده از کدپذیری

Cloud Firestore از مجموعه گسترده ای از انواع داده ها، از رشته های ساده گرفته تا نقشه های تو در تو پشتیبانی می کند. بیشتر این موارد مستقیماً با انواع داخلی سوئیفت مطابقت دارند. بیایید ابتدا نگاهی به نقشه برداری از انواع داده های ساده بیندازیم قبل از اینکه به انواع پیچیده تر بپردازیم.

برای نگاشت اسناد Cloud Firestore به انواع Swift، این مراحل را دنبال کنید:

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

ذخیره تاریخ در یک سند 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)

برای کسب اطلاعات بیشتر در مورد استعلام اسناد بر اساس مکان فیزیکی، این راهنمای راه حل را بررسی کنید.

Enums

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

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

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

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

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

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

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

نگران نباشید: Codable ما را تحت پوشش قرار داده است!

برای مواردی مانند این، می توانیم از CodingKeys استفاده کنیم. این یک عدد است که می‌توانیم آن را به یک ساختار کدگذاری اضافه کنیم تا مشخص کنیم که چگونه ویژگی‌های خاص نگاشت می‌شوند.

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

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

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

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

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

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

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

با این کار، می‌توانیم کدها را برای تگ‌های نگاشت به‌روزرسانی کنیم، به‌جای اینکه مجبور باشیم آن‌ها را به‌صورت دستی در کد UI برنامه‌مان نگاشت کنیم، مدیریت مستقیم رنگ‌های برچسب را آسان‌تر می‌کنیم:

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 استفاده کن!

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

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

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

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

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