সুইফট কোডেবল সহ ক্লাউড ফায়ারস্টোর ডেটা ম্যাপ করুন

সুইফট ৪-এ প্রবর্তিত সুইফটের কোডেবল এপিআই আমাদের কম্পাইলারের শক্তি ব্যবহার করে সিরিয়ালাইজড ফর্ম্যাট থেকে সুইফট টাইপে ডেটা ম্যাপ করা সহজ করে তোলে।

আপনি হয়তো কোনও ওয়েব এপিআই থেকে আপনার অ্যাপের ডেটা মডেলে ডেটা ম্যাপ করার জন্য কোডেবল ব্যবহার করছেন (এবং বিপরীতভাবে), কিন্তু এটি তার চেয়ে অনেক বেশি নমনীয়।

এই নির্দেশিকায়, আমরা দেখব কিভাবে Codable ব্যবহার করে Cloud Firestore থেকে সুইফট টাইপের ডেটা ম্যাপ করা যায় এবং এর বিপরীতটিও।

Cloud Firestore থেকে একটি ডকুমেন্ট আনার সময়, আপনার অ্যাপটি কী/মান জোড়ার একটি অভিধান পাবে (অথবা অভিধানের একটি অ্যারে, যদি আপনি একাধিক ডকুমেন্ট ফেরত দেওয়ার জন্য কোনও একটি অপারেশন ব্যবহার করেন)।

এখন, আপনি অবশ্যই Swift-এ সরাসরি অভিধান ব্যবহার চালিয়ে যেতে পারেন, এবং তারা কিছু দুর্দান্ত নমনীয়তা প্রদান করে যা আপনার ব্যবহারের ক্ষেত্রে ঠিক তাই হতে পারে। তবে, এই পদ্ধতিটি মোটেও নিরাপদ নয় এবং অ্যাট্রিবিউটের নাম ভুল বানান করে, অথবা গত সপ্তাহে আপনার দল যখন এই উত্তেজনাপূর্ণ নতুন বৈশিষ্ট্যটি প্রেরণ করেছিল তখন যে নতুন বৈশিষ্ট্যটি যোগ করেছিল তা ম্যাপ করতে ভুলে গিয়ে, ট্র্যাক-টু-ডাউন বাগগুলি প্রবর্তন করা সহজ।

অতীতে, অনেক ডেভেলপার একটি সহজ ম্যাপিং স্তর প্রয়োগ করে এই ত্রুটিগুলি দূর করেছেন যা তাদের Swift প্রকারগুলিতে অভিধান ম্যাপ করার অনুমতি দিয়েছে। কিন্তু আবারও, এই বাস্তবায়নগুলির বেশিরভাগই Cloud Firestore ডকুমেন্ট এবং আপনার অ্যাপের ডেটা মডেলের সংশ্লিষ্ট প্রকারের মধ্যে ম্যাপিং ম্যানুয়ালি নির্দিষ্ট করার উপর ভিত্তি করে।

Cloud Firestore সুইফটের কোডেবল এপিআই সমর্থনের মাধ্যমে, এটি অনেক সহজ হয়ে যায়:

  • আপনাকে আর কোনও ম্যাপিং কোড ম্যানুয়ালি প্রয়োগ করতে হবে না।
  • বিভিন্ন নামের সাথে বৈশিষ্ট্যগুলি কীভাবে ম্যাপ করতে হয় তা সংজ্ঞায়িত করা সহজ।
  • এতে সুইফটের অনেক ধরণের জন্য অন্তর্নির্মিত সমর্থন রয়েছে।
  • এবং কাস্টম প্রকারের ম্যাপিংয়ের জন্য সমর্থন যোগ করা সহজ।
  • সবচেয়ে ভালো কথা: সাধারণ ডেটা মডেলের জন্য, আপনাকে কোনও ম্যাপিং কোড লিখতে হবে না।

ম্যাপিং ডেটা

Cloud Firestore এমন ডকুমেন্টে ডেটা সঞ্চয় করে যা কীগুলিকে মানগুলিতে ম্যাপ করে। একটি পৃথক ডকুমেন্ট থেকে ডেটা আনতে, আমরা DocumentSnapshot.data() কল করতে পারি, যা ক্ষেত্রের নামগুলি ম্যাপ করে একটি অভিধান ফেরত দেয় Any : func data() -> [String : Any]?

এর মানে হল আমরা প্রতিটি পৃথক ক্ষেত্র অ্যাক্সেস করতে সুইফটের সাবস্ক্রিপ্ট সিনট্যাক্স ব্যবহার করতে পারি।

import FirebaseFirestore

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

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

যদিও এটি সহজবোধ্য এবং বাস্তবায়ন করা সহজ বলে মনে হতে পারে, এই কোডটি ভঙ্গুর, রক্ষণাবেক্ষণ করা কঠিন এবং ত্রুটি-প্রবণ।

আপনি দেখতে পাচ্ছেন, আমরা ডকুমেন্ট ফিল্ডের ডেটা টাইপ সম্পর্কে অনুমান করছি। এগুলো সঠিক হতেও পারে আবার নাও হতে পারে।

মনে রাখবেন, যেহেতু কোনও স্কিমা নেই, তাই আপনি সহজেই সংগ্রহে একটি নতুন ডকুমেন্ট যোগ করতে পারেন এবং একটি ফিল্ডের জন্য একটি ভিন্ন ধরণের নির্বাচন করতে পারেন। আপনি ভুলবশত numberOfPages ফিল্ডের জন্য স্ট্রিং বেছে নিতে পারেন, যার ফলে ম্যাপিং খুঁজে পাওয়া কঠিন হতে পারে। এছাড়াও, যখনই কোনও নতুন ফিল্ড যোগ করা হবে তখন আপনাকে আপনার ম্যাপিং কোড আপডেট করতে হবে, যা বেশ ঝামেলার।

এবং ভুলে গেলে চলবে না যে আমরা Swift এর শক্তিশালী টাইপ সিস্টেমের সুবিধা নিচ্ছি না, যা Book এর প্রতিটি বৈশিষ্ট্যের জন্য সঠিক টাইপটি সঠিকভাবে জানে।

যাইহোক, কোডেবল কী?

অ্যাপলের ডকুমেন্টেশন অনুসারে, কোডেবল হল "একটি টাইপ যা নিজেকে একটি বহিরাগত উপস্থাপনায় রূপান্তর করতে পারে এবং বাইরেও রূপান্তর করতে পারে।" প্রকৃতপক্ষে, কোডেবল হল এনকোডেবল এবং ডিকোডেবল প্রোটোকলের একটি টাইপ উপনাম। এই প্রোটোকলের সাথে একটি সুইফট টাইপ সামঞ্জস্য করে, কম্পাইলার JSON এর মতো একটি সিরিয়ালাইজড ফর্ম্যাট থেকে এই ধরণের একটি উদাহরণ এনকোড/ডিকোড করার জন্য প্রয়োজনীয় কোড সংশ্লেষিত করবে।

একটি বই সম্পর্কে তথ্য সংরক্ষণের জন্য একটি সহজ ধরণ এইরকম দেখতে হতে পারে:

struct Book: Codable {
  var title: String
  var numberOfPages: Int
  var author: String
}

আপনি দেখতে পাচ্ছেন, কোডেবলের সাথে টাইপটি কনফার্ম করা ন্যূনতম আক্রমণাত্মক। আমাদের কেবল প্রোটোকলে কনফার্মেন্স যোগ করতে হয়েছিল; অন্য কোনও পরিবর্তনের প্রয়োজন ছিল না।

এটি চালু করার সাথে সাথে, আমরা এখন সহজেই একটি বইকে 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 ডকুমেন্টগুলিকে সুইফট টাইপের সাথে ম্যাপ করতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. আপনার প্রোজেক্টে FirebaseFirestore ফ্রেমওয়ার্ক যোগ করেছেন কিনা তা নিশ্চিত করুন। আপনি এটি করার জন্য Swift Package Manager অথবা CocoaPods ব্যবহার করতে পারেন।
  2. আপনার Swift ফাইলে FirebaseFirestore আমদানি করুন।
  3. আপনার টাইপটি Codable এর সাথে সামঞ্জস্য করুন।
  4. (ঐচ্ছিক, যদি আপনি List ভিউতে টাইপটি ব্যবহার করতে চান) আপনার টাইপে একটি id প্রপার্টি যোগ করুন এবং @DocumentID ব্যবহার করে Cloud Firestore ডকুমেন্ট আইডিতে এটি ম্যাপ করতে বলুন। আমরা নীচে আরও বিস্তারিতভাবে এটি নিয়ে আলোচনা করব।
  5. একটি সুইফট টাইপের ডকুমেন্ট রেফারেন্স ম্যাপ করতে documentReference.data(as: ) ব্যবহার করুন।
  6. সুইফট টাইপ থেকে Cloud Firestore ডকুমেন্টে ডেটা ম্যাপ করতে documentReference.setData(from: ) ব্যবহার করুন।
  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 এটি করার সবচেয়ে সহজ উপায় হল একটি মানচিত্র ব্যবহার করা:

একটি ফায়ারস্টোর ডকুমেন্টে একটি নেস্টেড কাস্টম টাইপ সংরক্ষণ করা

সংশ্লিষ্ট সুইফট স্ট্রাক্ট লেখার সময়, আমরা এই সত্যটি ব্যবহার করতে পারি যে 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?
}

লক্ষ্য করুন, Cloud Firestore ডকুমেন্টে কভার ম্যাপের জন্য আমরা একটি স্ট্রাক্ট, CoverImages কীভাবে সংজ্ঞায়িত করেছি। BookWithCoverImages এ কভার প্রপার্টিটিকে ঐচ্ছিক হিসেবে চিহ্নিত করে, আমরা এই বিষয়টি মোকাবেলা করতে সক্ষম যে কিছু ডকুমেন্টে কভার অ্যাট্রিবিউট নাও থাকতে পারে।

যদি আপনি জানতে আগ্রহী হন যে ডেটা আনা বা আপডেট করার জন্য কোনও কোড স্নিপেট নেই, তাহলে আপনি জেনে খুশি হবেন যে Cloud Firestore থেকে/এ পড়ার বা লেখার জন্য কোডটি সামঞ্জস্য করার কোনও প্রয়োজন নেই: এই সমস্ত কিছুই প্রাথমিক বিভাগে আমরা যে কোডটি লিখেছি তার সাথে কাজ করে।

অ্যারে

কখনও কখনও, আমরা একটি নথিতে মূল্যবোধের একটি সংগ্রহ সংরক্ষণ করতে চাই। একটি বইয়ের ধরণগুলি এর একটি ভালো উদাহরণ: The Hitchhiker's Guide to the Galaxy এর মতো একটি বই বিভিন্ন বিভাগে পড়তে পারে — এই ক্ষেত্রে "Sci-Fi" এবং "Comedy":

একটি ফায়ারস্টোর ডকুমেন্টে একটি অ্যারে সংরক্ষণ করা

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

যেহেতু এটি যেকোনো কোডেবল টাইপের জন্য কাজ করে, তাই আমরা কাস্টম টাইপও ব্যবহার করতে পারি। ধরুন আমরা প্রতিটি বইয়ের জন্য ট্যাগের একটি তালিকা সংরক্ষণ করতে চাই। ট্যাগের নামের সাথে, আমরা ট্যাগের রঙও সংরক্ষণ করতে চাই, যেমন:

একটি ফায়ারস্টোর ডকুমেন্টে কাস্টম ধরণের একটি অ্যারে সংরক্ষণ করা

এইভাবে ট্যাগ সংরক্ষণ করার জন্য, আমাদের যা করতে হবে তা হল একটি Tag স্ট্রাক্ট বাস্তবায়ন করে একটি ট্যাগ উপস্থাপন করা এবং এটি কোডেবল করা:

struct Tag: Codable, Hashable {
  var title: String
  var color: String
}

এবং ঠিক তেমনই, আমরা আমাদের Book ডকুমেন্টে Tags একটি অ্যারে সংরক্ষণ করতে পারি!

struct BookWithTags: Codable {
  @DocumentID var id: String?
  var title: String
  var numberOfPages: Int
  var author: String
  var tags: [Tag]
}

ডকুমেন্ট আইডি ম্যাপিং সম্পর্কে কিছু কথা

আরও ধরণের ম্যাপিংয়ের দিকে এগিয়ে যাওয়ার আগে, আসুন ডকুমেন্ট আইডি ম্যাপিং সম্পর্কে কিছুক্ষণ আলোচনা করি।

আমরা পূর্ববর্তী কিছু উদাহরণে @DocumentID প্রপার্টি র‍্যাপার ব্যবহার করে আমাদের Cloud Firestore ডকুমেন্টের ডকুমেন্ট আইডিকে আমাদের সুইফট টাইপের id প্রপার্টির সাথে ম্যাপ করেছি। এটি বেশ কয়েকটি কারণে গুরুত্বপূর্ণ:

  • ব্যবহারকারী যদি স্থানীয় পরিবর্তন করে, তাহলে কোন ডকুমেন্টটি আপডেট করতে হবে তা জানতে এটি আমাদের সাহায্য করে।
  • SwiftUI-এর List উপাদানগুলিকে Identifiable হতে হবে যাতে ঢোকানোর সময় উপাদানগুলি এদিক-ওদিক না ছড়িয়ে পড়ে।

এটা উল্লেখ করা দরকার যে @DocumentID হিসেবে চিহ্নিত একটি অ্যাট্রিবিউট ডকুমেন্টটি লেখার সময় Cloud Firestore এনকোডার দ্বারা এনকোড করা হবে না। কারণ ডকুমেন্ট আইডিটি ডকুমেন্টের নিজস্ব অ্যাট্রিবিউট নয় — তাই ডকুমেন্টে এটি লেখা ভুল হবে।

নেস্টেড টাইপের সাথে কাজ করার সময় (যেমন এই নির্দেশিকার আগের উদাহরণে Book ট্যাগের অ্যারে), @DocumentID প্রোপার্টি যোগ করার প্রয়োজন নেই: নেস্টেড প্রোপার্টিগুলি Cloud Firestore ডকুমেন্টের একটি অংশ, এবং একটি পৃথক ডকুমেন্ট গঠন করে না। অতএব, তাদের একটি ডকুমেন্ট আইডির প্রয়োজন হয় না।

তারিখ এবং সময়

Cloud Firestore তারিখ এবং সময় পরিচালনা করার জন্য একটি অন্তর্নির্মিত ডেটা টাইপ রয়েছে এবং কোডেবলের জন্য Cloud Firestore সমর্থনের জন্য ধন্যবাদ, এগুলি ব্যবহার করা সহজ।

আসুন এই ডকুমেন্টটি একবার দেখে নেওয়া যাক যা ১৮৪৩ সালে আবিষ্কৃত সকল প্রোগ্রামিং ভাষার জননী, অ্যাডা-কে প্রতিনিধিত্ব করে:

ফায়ারস্টোর ডকুমেন্টে তারিখ সংরক্ষণ করা

এই ডকুমেন্টটি ম্যাপ করার জন্য একটি সুইফট টাইপ এইরকম দেখতে হতে পারে:

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-এর একটি সাধারণ ব্যবহার হল কোনও কিছুর বিচ্ছিন্ন অবস্থা মডেল করা। উদাহরণস্বরূপ, আমরা হয়তো নিবন্ধ পরিচালনার জন্য একটি অ্যাপ লিখছি। একটি নিবন্ধের অবস্থা ট্র্যাক করার জন্য, আমরা একটি enum Status ব্যবহার করতে চাইতে পারি:

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

Cloud Firestore এনামগুলিকে নেটিভভাবে সমর্থন করে না (অর্থাৎ, এটি মানগুলির সেট প্রয়োগ করতে পারে না), তবে আমরা এখনও এই সত্যটি ব্যবহার করতে পারি যে এনামগুলি টাইপ করা যেতে পারে এবং একটি কোডেবল টাইপ বেছে নিতে পারি। এই উদাহরণে, আমরা String বেছে নিয়েছি, যার অর্থ হল Cloud Firestore ডকুমেন্টে সংরক্ষণ করা হলে সমস্ত এনাম মান স্ট্রিং-এ/থেকে ম্যাপ করা হবে।

এবং, যেহেতু Swift কাস্টম raw মান সমর্থন করে, আমরা এমনকি কোন মানগুলি কোন enum কেসকে নির্দেশ করে তা কাস্টমাইজ করতে পারি। উদাহরণস্বরূপ, যদি আমরা Status.inReview কেসটিকে "review" হিসাবে সংরক্ষণ করার সিদ্ধান্ত নিই, তাহলে আমরা উপরের enumটি নিম্নরূপ আপডেট করতে পারি:

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

ম্যাপিং কাস্টমাইজ করা

কখনও কখনও, আমরা যে Cloud Firestore ডকুমেন্টগুলি ম্যাপ করতে চাই তার অ্যাট্রিবিউটের নামগুলি আমাদের সুইফটের ডেটা মডেলের বৈশিষ্ট্যগুলির নামের সাথে মেলে না। উদাহরণস্বরূপ, আমাদের একজন সহকর্মী পাইথন ডেভেলপার হতে পারেন এবং তাদের সমস্ত অ্যাট্রিবিউট নামের জন্য snake_case বেছে নেওয়ার সিদ্ধান্ত নেন।

চিন্তার কিছু নেই: কোডেবল আমাদের সব কিছুর সমাধান করে দিয়েছে!

এই ধরণের ক্ষেত্রে, আমরা CodingKeys ব্যবহার করতে পারি। এটি একটি enum যা আমরা একটি কোডেবল স্ট্রাক্টে যোগ করতে পারি যাতে নির্দিষ্ট বৈশিষ্ট্যগুলি কীভাবে ম্যাপ করা হবে তা নির্দিষ্ট করা যায়।

এই নথিটি বিবেচনা করুন:

snake_cased অ্যাট্রিবিউটের নাম সহ একটি Firestore ডকুমেন্ট

এই ডকুমেন্টটিকে এমন একটি স্ট্রাক্টের সাথে ম্যাপ করার জন্য যার name প্রোপার্টি String টাইপের, আমাদের ProgrammingLanguage struct-এ একটি CodingKeys enum যোগ করতে হবে এবং ডকুমেন্টে অ্যাট্রিবিউটের নাম উল্লেখ করতে হবে:

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 প্রোপার্টি সংজ্ঞায়িত করেছি যা আমরা SwiftUI List ভিউতে শনাক্তকারী হিসেবে ব্যবহার করতে চাইতে পারি। যদি আমরা এটি CodingKeys এ নির্দিষ্ট না করি, তাহলে ডেটা আনার সময় এটি ম্যাপ করা হবে না এবং এর ফলে nil হয়ে যাবে। এর ফলে List ভিউটি প্রথম ডকুমেন্ট দিয়ে পূর্ণ হবে।

সংশ্লিষ্ট CodingKeys enum-এ কেস হিসেবে তালিকাভুক্ত না থাকা যেকোনো সম্পত্তি ম্যাপিং প্রক্রিয়ার সময় উপেক্ষা করা হবে। আমরা যদি নির্দিষ্টভাবে কিছু বৈশিষ্ট্য ম্যাপ করা থেকে বাদ দিতে চাই, তাহলে এটি আসলে সুবিধাজনক হতে পারে।

উদাহরণস্বরূপ, যদি আমরা reasonWhyILoveThis প্রোপার্টিটি ম্যাপ করা থেকে বাদ দিতে চাই, তাহলে আমাদের যা করতে হবে তা হল CodingKeys enum থেকে এটি সরিয়ে ফেলা:

struct ProgrammingLanguage: Identifiable, Codable {
  @DocumentID var id: String?
  var name: String
  var year: Date
  var reasonWhyILoveThis: String = ""
  
  enum CodingKeys: String, CodingKey {
    case id
    case name = "language_name"
    case year
  }
}

মাঝে মাঝে আমরা Cloud Firestore ডকুমেন্টে একটি খালি অ্যাট্রিবিউট লিখতে চাইতে পারি। সুইফটের ধারণা হল "optionals" মানে হল "মান অনুপস্থিতি", এবং Cloud Firestore null মানও সমর্থন করে। যাইহোক, nil মান সম্পন্ন optional গুলিকে এনকোড করার জন্য ডিফল্ট আচরণ হল কেবল সেগুলিকে বাদ দেওয়া। @ExplicitNull আমাদের Swift optional গুলিকে এনকোড করার সময় কীভাবে পরিচালনা করা হয় তার উপর কিছু নিয়ন্ত্রণ দেয়: একটি ঐচ্ছিক সম্পত্তিকে @ExplicitNull হিসাবে চিহ্নিত করে, আমরা Cloud Firestore এই সম্পত্তিটি একটি null মান দিয়ে ডকুমেন্টে লিখতে বলতে পারি যদি এতে nil মান থাকে।

রঙ ম্যাপিংয়ের জন্য একটি কাস্টম এনকোডার এবং ডিকোডার ব্যবহার করা

কোডেবলের মাধ্যমে ডেটা ম্যাপিং সম্পর্কে আমাদের কভারেজের শেষ বিষয় হিসেবে, আসুন কাস্টম এনকোডার এবং ডিকোডারের সাথে পরিচয় করিয়ে দেই। এই বিভাগটি কোনও স্থানীয় Cloud Firestore ডেটাটাইপকে অন্তর্ভুক্ত করে না, তবে কাস্টম এনকোডার এবং ডিকোডারগুলি আপনার Cloud Firestore অ্যাপগুলিতে ব্যাপকভাবে কার্যকর।

"আমি রঙ কীভাবে ম্যাপ করতে পারি" হল ডেভেলপারদের সবচেয়ে ঘন ঘন জিজ্ঞাসিত প্রশ্নগুলির মধ্যে একটি, কেবল Cloud Firestore জন্যই নয়, সুইফট এবং JSON এর মধ্যে ম্যাপিংয়ের জন্যও। প্রচুর সমাধান রয়েছে, তবে বেশিরভাগই JSON এর উপর ফোকাস করে এবং প্রায় সবগুলিই RGB উপাদানগুলির সমন্বয়ে গঠিত একটি নেস্টেড অভিধান হিসাবে রঙ ম্যাপ করে।

মনে হচ্ছে এর থেকে আরও ভালো, সহজ সমাধান থাকা উচিত। কেন আমরা ওয়েব রঙ (অথবা, আরও স্পষ্ট করে বলতে গেলে, CSS হেক্স রঙের স্বরলিপি) ব্যবহার করব না — এগুলি ব্যবহার করা সহজ (মূলত কেবল একটি স্ট্রিং), এবং এমনকি তারা স্বচ্ছতাও সমর্থন করে!

একটি Swift Color তার হেক্স মানের সাথে ম্যাপ করতে, আমাদের একটি Swift এক্সটেনশন তৈরি করতে হবে যা Color এ Codable যোগ করবে।

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() ব্যবহার করে, আমরা RGBA উপাদানগুলিকে নেস্ট না করেই একটি String কে তার Color সমতুল্য ডিকোড করতে পারি। এছাড়াও, আপনি এই মানগুলিকে আপনার অ্যাপের ওয়েব 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 যখনই কোনও আপডেট আসবে তখন আমাদের শ্রোতাকে কল করবে।

এখানে একটি কোড স্নিপেট দেওয়া হল যা দেখায় কিভাবে স্ন্যাপশট লিসেনার নিবন্ধন করতে হয়, কোডেবল ব্যবহার করে ডেটা ম্যাপ করতে হয় এবং যেকোনো ত্রুটি মোকাবেলা করতে হয়। এটি সংগ্রহে একটি নতুন ডকুমেন্ট কীভাবে যুক্ত করতে হয় তাও দেখায়। আপনি দেখতে পাবেন, ম্যাপ করা ডকুমেন্টগুলি ধারণ করে স্থানীয় অ্যারে আপডেট করার কোনও প্রয়োজন নেই, কারণ এটি স্ন্যাপশট লিসেনারের কোড দ্বারা যত্ন নেওয়া হয়।

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 সংগ্রহস্থল থেকে ডাউনলোড করতে পারেন।

এগিয়ে যান এবং কোডেবল ব্যবহার করুন!

সুইফটের কোডেবল এপিআই সিরিয়ালাইজড ফর্ম্যাট থেকে আপনার অ্যাপ্লিকেশন ডেটা মডেলে ডেটা ম্যাপ করার একটি শক্তিশালী এবং নমনীয় উপায় প্রদান করে। এই নির্দেশিকায়, আপনি দেখেছেন যে Cloud Firestore তাদের ডেটাস্টোর হিসাবে ব্যবহার করে এমন অ্যাপগুলিতে এটি ব্যবহার করা কতটা সহজ।

সাধারণ ডেটা টাইপের একটি মৌলিক উদাহরণ থেকে শুরু করে, আমরা ধীরে ধীরে ডেটা মডেলের জটিলতা বৃদ্ধি করেছি, একই সাথে আমাদের জন্য ম্যাপিং সম্পাদনের জন্য কোডেবল এবং ফায়ারবেসের বাস্তবায়নের উপর নির্ভর করতে সক্ষম হয়েছি।

কোডেবল সম্পর্কে আরও বিস্তারিত জানার জন্য, আমি নিম্নলিখিত সংস্থানগুলি সুপারিশ করছি:

যদিও আমরা Cloud Firestore ডকুমেন্ট ম্যাপ করার জন্য একটি বিস্তৃত নির্দেশিকা সংকলন করার জন্য যথাসাধ্য চেষ্টা করেছি, এটি সম্পূর্ণ নয় এবং আপনি আপনার প্রকারগুলি ম্যাপ করার জন্য অন্যান্য কৌশল ব্যবহার করতে পারেন। নীচের প্রতিক্রিয়া পাঠান বোতামটি ব্যবহার করে, অন্যান্য ধরণের Cloud Firestore ডেটা ম্যাপ করার জন্য বা সুইফটে ডেটা উপস্থাপন করার জন্য আপনি কোন কৌশলগুলি ব্যবহার করেন তা আমাদের জানান।

Cloud Firestore কোডেবল সাপোর্ট ব্যবহার না করার আসলে কোনও কারণ নেই।