Swift Codable-এর সাহায্যে Cloud Firestore ডেটা ম্যাপ করা

Swift 4-এ চালু করা Swift-এর Codable API, সিরিয়ালাইজ করা ফর্ম্যাট থেকে Swift-এর প্রকারে ডেটা ম্যাপ করার কাজ সহজ করতে, কম্পাইলারের ক্ষমতাকে কাজে লাগাতে আমাদের সাহায্য করে।

আপনি হয়ত Codable ব্যবহার করে কোনও ওয়েব API থেকে আপনার অ্যাপের ডেটা মডেলে ডেটা ম্যাপ করছেন (এবং উল্টোটাও), কিন্তু এটি তার চেয়ে অনেক বেশি ফ্লেক্সিবল।

এই গাইডে, আমরা দেখব কীভাবে Codable ব্যবহার করে Cloud Firestore থেকে Swift টাইপে ডেটা ম্যাপ করা যায় এবং এর উল্টোটাও করা যায়।

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

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

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

Swift-এর Codable API-এর জন্য Cloud Firestore-এর সহায়তা থাকায়, এটি অনেক সহজ হয়ে যায়:

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

ম্যাপিং ডেটা

Cloud Firestore ডকুমেন্টে ডেটা স্টোর করে যা ভ্যালুতে 'কী' ম্যাপ করে। কোনও ব্যক্তিগত ডকুমেন্ট থেকে ডেটা পেতে, আমরা DocumentSnapshot.data()-কে কল করতে পারি, যা Any-এ ফিল্ডের নাম ম্যাপ করে একটি ডিকশনারি রিটার্ন করে: func data() -> [String : Any]?.

এর অর্থ হল, প্রতিটি ফিল্ড অ্যাক্সেস করার জন্য আমরা Swift-এর সাবস্ক্রিপ্ট সিনট্যাক্স ব্যবহার করতে পারি।

import FirebaseFirestore

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

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

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

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

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

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

Codable আসলে কী?

Apple-এর ডকুমেন্টেশন অনুযায়ী, Codable হল "এমন একটি টাইপ যা নিজেকে এক্সটার্নাল রিপ্রেজেন্টেশনে এবং তা থেকে কনভার্ট করতে পারে।" আসলে, Codable হল Encodable এবং Decodable প্রোটোকলের জন্য এক ধরনের উপনাম। এই প্রোটোকলে Swift টাইপ কনফর্ম করার মাধ্যমে, কম্পাইলার JSON-এর মতো সিরিয়ালাইজ করা ফর্ম্যাট থেকে এই ধরনের ইনস্ট্যান্স এনকোড/ডিকোড করার জন্য প্রয়োজনীয় কোড সিন্থেসাইজ করবে।

কোনও বই সম্পর্কে ডেটা স্টোর করার জন্য একটি সাধারণ ধরন এইরকম দেখতে হতে পারে:

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

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

এটি হয়ে গেলে, আমরা এখন সহজেই একটি বইকে JSON অবজেক্টে এনকোড করতে পারি:

do {
  let book = Book(title: "The Hitchhiker's Guide to the Galaxy",
                  numberOfPages: 816,
                  author: "Douglas Adams")
  let encoder = JSONEncoder()
  let data = try encoder.encode(book)
}
catch {
  print("Error when trying to encode book: \(error)")
}

Book ইনস্ট্যান্সে JSON অবজেক্ট ডিকোড করার পদ্ধতি নিচে দেওয়া হল:

let decoder = JSONDecoder()
let data = /* fetch data from the network */
let decodedBook = try decoder.decode(Book.self, from: data)

Codable ব্যবহার করে Cloud Firestore ডকুমেন্টে সহজ ধরনের ম্যাপিং করা

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

Swift টাইপে Cloud Firestore ডকুমেন্ট ম্যাপ করতে, এই ধাপগুলি অনুসরণ করুন:

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

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

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

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

অ্যারে

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

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
}

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

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

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

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

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

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

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

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

তারিখ ও সময়

তারিখ ও সময় ম্যানেজ করার জন্য Cloud Firestore-এ বিল্ট-ইন ডেটা টাইপ আছে এবং Cloud Firestore-এ Codable-এর জন্য সাপোর্ট থাকার কারণে, এগুলি ব্যবহার করা খুবই সহজ।

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

Firestore ডকুমেন্টে তারিখ স্টোর করা

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

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

@ServerTimestamp সম্পর্কে আলোচনা না করে তারিখ ও সময় সংক্রান্ত এই বিভাগটি আমরা ছেড়ে যেতে পারি না। আপনার অ্যাপে টাইমস্ট্যাম্প নিয়ে কাজ করার সময় এই প্রপার্টি র‍্যাপারটি অত্যন্ত কার্যকর।

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

Cloud Firestore, @ServerTimestamp দিয়ে চিহ্নিত অ্যাট্রিবিউটকে এইভাবে ম্যানেজ করে: আপনি এটি স্টোর করার সময় (যেমন, addDocument() ব্যবহার করে) অ্যাট্রিবিউটটি nil হলে, ডেটাবেসে লেখার সময় Cloud Firestore বর্তমান সার্ভার টাইমস্ট্যাম্প দিয়ে ফিল্ডটি পূরণ করবে। addDocument() বা updateData()-কে কল করার সময় ফিল্ডটি nil না হলে, Cloud Firestore অ্যাট্রিবিউটের মানে কোনও পরিবর্তন করবে না। এইভাবে, createdAt এবং lastUpdatedAt-এর মতো ফিল্ড সহজেই প্রয়োগ করা যায়।

জিওপয়েন্ট

আমাদের অ্যাপে জিওলোকেশন সর্বত্রই রয়েছে। এগুলি সেভ করে রাখার ফলে অনেক আকর্ষণীয় ফিচার ব্যবহার করা সম্ভব হয়। যেমন, কোনও টাস্কের জন্য লোকেশন স্টোর করা উপযোগী হতে পারে যাতে আপনি গন্তব্যে পৌঁছালে আপনার অ্যাপ আপনাকে কোনও টাস্কের কথা মনে করিয়ে দিতে পারে।

Cloud Firestore-এ একটি বিল্ট-ইন ডেটা টাইপ, GeoPoint আছে, যা যেকোনও লোকেশনের দ্রাঘিমাংশ এবং অক্ষাংশ স্টোর করতে পারে। Cloud Firestore ডকুমেন্ট থেকে/পর্যন্ত লোকেশন ম্যাপ করতে, আমরা GeoPoint ধরন ব্যবহার করতে পারি:

struct Office: Codable {
  @DocumentID var id: String?
  var name: String
  var location: GeoPoint
}

Swift-এ সংশ্লিষ্ট ধরন হল CLLocationCoordinate2D এবং আমরা নিম্নলিখিত অপারেশন সহ এই দুটি ধরনের মধ্যে ম্যাপ করতে পারি:

CLLocationCoordinate2D(latitude: office.location.latitude,
                      longitude: office.location.longitude)

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

Enums

Enums সম্ভবত Swift-এর সবচেয়ে অবমূল্যায়িত ভাষা ফিচার; এতে যা দেখা যায় তার চেয়ে অনেক বেশি কিছু আছে। enum-এর একটি সাধারণ ব্যবহার হল কোনও কিছুর বিচ্ছিন্ন অবস্থাকে মডেল করা। যেমন, আমরা নিবন্ধ ম্যানেজ করার জন্য একটি অ্যাপ তৈরি করতে পারি। কোনও নিবন্ধের স্ট্যাটাস ট্র্যাক করতে, আমরা হয়ত একটি enum Status ব্যবহার করতে চাইব:

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

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

এবং, যেহেতু Swift কাস্টম র ভ্যালু সাপোর্ট করে, তাই আমরা এমনকি কাস্টমাইজ করতে পারি যে কোন ভ্যালু কোন enum কেসকে রেফার করে। তাই, উদাহরণস্বরূপ, আমরা যদি Status.inReview কেস "পর্যালোচনাধীন" হিসেবে স্টোর করার সিদ্ধান্ত নিই, তাহলে আমরা উপরের enum এইভাবে আপডেট করতে পারি:

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

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

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

চিন্তা করবেন না: Codable আমাদের সাহায্য করবে!

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

এই ডকুমেন্টটি বিবেচনা করুন:

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

এই ডকুমেন্টকে String ধরনের নাম প্রপার্টি আছে এমন স্ট্রাক্টে ম্যাপ করতে, আমাদের ProgrammingLanguage স্ট্রাক্টে একটি 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 ডকুমেন্টে একটি খালি অ্যাট্রিবিউট লিখতে চাইতে পারি। Swift-এ ভ্যালু না থাকার বিষয়টি বোঝাতে ঐচ্ছিক ভ্যালুর ধারণা আছে এবং Cloud Firestore-এ null ভ্যালুও কাজ করে। তবে, nil ভ্যালু আছে এমন ঐচ্ছিক এনকোডিংয়ের ক্ষেত্রে ডিফল্ট অ্যাকশন হল সেগুলি বাদ দেওয়া। @ExplicitNull আমাদের কিছু নিয়ন্ত্রণ দেয় যে কীভাবে Swift এনকোড করার সময় ঐচ্ছিকগুলি পরিচালনা করা হয়: একটি ঐচ্ছিক সম্পত্তি হিসাবে ফ্ল্যাগ করে @ExplicitNull, আমরা Cloud Firestore কে এই প্রপার্টিটি লিখতে বলতে পারি নথিতে একটি শূন্য মান সহ যদি এতে nil এর মান থাকে।

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

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

"আমি কীভাবে রঙ ম্যাপ করতে পারি" হল ডেভেলপারদের সবচেয়ে বেশি জিজ্ঞাসিত প্রশ্নগুলির মধ্যে একটি, শুধু Cloud Firestore-এর জন্য নয়, Swift এবং 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() ব্যবহার করে, আমরা 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 ব্যবহার করুন!

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

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

Codable সম্পর্কে আরও জানতে, আমি নিম্নলিখিত রিসোর্সগুলি সাজেস্ট করছি:

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

Cloud Firestore-এর Codable সাপোর্ট ব্যবহার না করার কোনও কারণ নেই।