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

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

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

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

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

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

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

با پشتیبانی Cloud Firestore از Codable API 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 انتخاب کنید، که منجر به مشکلی برای یافتن نقشه‌برداری می‌شود. همچنین، هر زمان که یک فیلد جدید اضافه می شود، باید کد نقشه برداری خود را به روز کنید، که نسبتاً دست و پا گیر است.

و فراموش نکنیم که ما از سیستم نوع قوی سوئیفت که دقیقا نوع صحیح هر یک از ویژگی های 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 استفاده می کنید.

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

،

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

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

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

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

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

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

با پشتیبانی Cloud Firestore از Codable API 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 انتخاب کنید، که منجر به مشکلی برای یافتن نقشه‌برداری می‌شود. همچنین، هر زمان که یک فیلد جدید اضافه می شود، باید کد نقشه برداری خود را به روز کنید، که نسبتاً دست و پا گیر است.

و فراموش نکنیم که ما از سیستم نوع قوی سوئیفت که دقیقا نوع صحیح هر یک از ویژگی های 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
  }
}

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

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

هر ملکی که به عنوان موردی در مورد 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 بگوییم که اگر حاوی مقدار nil باشد ، این ویژگی را با یک مقدار تهی بنویسد.

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

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

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

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

برای اینکه بتوانیم یک Color سریع را به مقدار سحر و جادو خود نقشه برداری کنیم ، باید یک پسوند سریع ایجاد کنیم که به 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 وب برنامه خود استفاده کنید ، بدون اینکه ابتدا آنها را تبدیل کنید!

با این کار ، ما می توانیم کد را برای نقشه برداری به روز کنیم ، و این باعث می شود که به جای اینکه آنها را به صورت دستی در کد 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 استفاده کنید!

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

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

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

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

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

،

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

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

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

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

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

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

با پشتیبانی Cloud Firestore از API Codable 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 انتخاب کنید ، که منجر به مسئله نقشه برداری دشوار می شود. همچنین ، شما باید هر زمان که زمینه جدیدی اضافه شود ، کد نقشه برداری خود را به روز کنید ، که کاملاً دست و پا گیر است.

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

به هر حال قابل رمزگذاری چیست؟

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

برای نقشه برداری از اسناد Cloud Firestore به انواع سریع ، این مراحل را دنبال کنید:

  1. اطمینان حاصل کنید که چارچوب FirebaseFirestore را به پروژه خود اضافه کرده اید. برای انجام این کار می توانید از مدیر بسته Swift یا 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 نیست: همه اینها با کد ما کار می کند در بخش اولیه نوشته شده است.

آرایه ها

بعضی اوقات ، ما می خواهیم مجموعه ای از مقادیر را در یک سند ذخیره کنیم. ژانرهای یک کتاب نمونه خوبی است: کتابی مانند راهنمای Hitchhiker برای کهکشان ممکن است در چند دسته قرار بگیرد-در این مورد "علمی تخیلی" و "کمدی":

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

یک نوع سریع برای نقشه برداری این سند ممکن است به این شکل باشد:

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

ما نمی توانیم بدون گفتگو در مورد @ServerTimestamp ، این بخش را در مورد تاریخ و زمانها ترک کنیم. این بسته بندی ملک در هنگام برخورد با جدول زمانی در برنامه شما یک نیروگاه است.

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

Cloud Firestore ویژگی های مشخص شده با @ServerTimestamp را به شرح زیر می گذارد: اگر این ویژگی هنگام ذخیره آن (به عنوان مثال با استفاده از addDocument() ) nil است ، Cloud Firestore در زمان نوشتن آن در پایگاه داده ، زمینه را با Timestamp سرور فعلی جمع می کند . اگر هنگام تماس با 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 احتمالاً یکی از کمترین ویژگی های زبان در Swift است. چیزهای بیشتری برای آنها وجود دارد تا اینکه چشم را ملاقات کند. یک مورد استفاده متداول برای Enums مدل سازی حالات گسسته چیزی است. به عنوان مثال ، ما ممکن است در حال نوشتن برنامه ای برای مدیریت مقالات باشیم. برای ردیابی وضعیت مقاله ، ممکن است بخواهیم از Status enum استفاده کنیم:

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

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

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

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

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

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

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

هر ملکی که به عنوان موردی در مورد 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 بگوییم که اگر حاوی مقدار nil باشد ، این ویژگی را با یک مقدار تهی بنویسد.

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

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

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

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

برای اینکه بتوانیم یک Color سریع را به مقدار سحر و جادو خود نقشه برداری کنیم ، باید یک پسوند سریع ایجاد کنیم که به 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 وب برنامه خود استفاده کنید ، بدون اینکه ابتدا آنها را تبدیل کنید!

با این کار ، ما می توانیم کد را برای نقشه برداری به روز کنیم ، و این باعث می شود که به جای اینکه آنها را به صورت دستی در کد 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 استفاده کنید!

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

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

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

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

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