แมปข้อมูล Cloud Firestore ด้วย Swift Codable

Codable API ของ Swift ซึ่งเปิดตั������ Swift 4 ����วยให้เราสามารถใช้พลังของ คอมไพเลอร์เพื่อแมปข้อมูลจากรูปแบบที่ต่อเนื่องกันไปยัง Swift ได้ง่ายขึ้น ประเภทต่างๆ

คุณอาจเคยใช้ Codable เพื่อจับคู่ข้อมูลจาก Web API กับข้อมูลของแอป (หรือกลับกัน) แต่โมเดลนี้มีความยืดหยุ่นกว่า

ในคู่มือนี้ เราจะดูวิธีใช้ Codable ในการแมปข้อมูลจาก Cloud Firestore เป็นประเภท Swift และในทางกลับกัน

เมื่อดึงข้อมูลเอกสารจาก Cloud Firestore แอปของคุณจะได้รับ พจนานุกรมของคู่คีย์/ค่า (หรืออาร์เรย์ของพจนานุกรม หากคุณใช้ การดำเนินการส่งกลับเอกสารหลายรายการ)

ตอนนี้คุณสามารถใช้พจนานุกรมใน Swift ได้โดยตรงและ ให้ความยืดหยุ่นที่ยอดเยี่ยมซึ่งเหมาะกับ Use Case ของคุณ อย่างไรก็ตาม วิธีการนี้ไม่ปลอดภัยและแนะนำได้ง่ายๆ ข้อบกพร่องที่ยากต่อการติดตามโดยการสะกดชื่อแอตทริบิวต์ผิดหรือลืมแมป แอตทริบิวต์ใหม่ที่ทีมของคุณเพิ่มไว้ เมื่อเปิดตัวฟีเจอร์ใหม่ที่น่าสนใจ เมื่อสัปดาห์ที่แล้ว

ในอดีต นักพัฒนาซอฟต์แวร์จำนวนมากได้แก้ไขข้อผิดพลาดเหล่านี้ การใช้เลเยอร์การแมปที่เรียบง่ายซึ่งช่วยให้พวกเขาสามารถจับคู่พจนานุกรม ประเภท Swift ขอย้ำอีกครั้งว่า การติดตั้งใช้งานเหล่านี้ส่วนใหญ่มักอิงตาม ระบุการแมประหว่างเอกสาร Cloud Firestore และ ประเภทที่เกี่ยวข้องของโมเดลข้อมูลของแอป

การรองรับ Codable API ของ Swift ของ 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 เป็นชื่อแทน สำหรับโปรโตคอลที่เข้ารหัสได้และถอดรหัสได้ การปฏิบัติตามประเภท Swift เป็นเช่นนี้ แล้วคอมไพเลอร์จะสังเคราะห์โค้ดที่จำเป็นต่อการเข้ารหัส/ถอดรหัส อินสแตนซ์ประเภทนี้จากรูปแบบที่ต่อเนื่องกัน เช่น 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
โดยใช้ Codable

Cloud Firestore รองรับชุดข้อมูลได้หลากหลายตั้งแต่ประเภท กับแผนที่ที่ซ้อนกันอยู่ ซึ่งส่วนใหญ่แล้วจะสอดคล้องกับ ตัวกรองภายในของ Swift โดยตรง ประเภทต่างๆ มาดูการแมปประเภทข้อมูลง่ายๆ บางส่วนกันก่อนที่จะเจาะลึก ให้เป็นรายการที่ซับซ้อนมากขึ้น

หากต้องการแมปเอกสาร Cloud Firestore กับประเภท Swift ให้ทำตามขั้นตอนต่อไปนี้

  1. ตรวจสอบว่าได้เพิ่มเฟรมเวิร์ก FirebaseFirestore ลงในโปรเจ็กต์แล้ว คุณสามารถใช้ Swift Package Manager หรือ CocoaPods ให้ทำเช่นนั้นได้
  2. นำเข้า FirebaseFirestore ลงในไฟล์ Swift
  3. ยืนยันประเภทเป็น Codable
  4. (ไม่บังคับ ถ้าคุณต้องการใช้ประเภทในมุมมอง List) เพิ่ม id ให้กับประเภท และใช้ @DocumentID เพื่อบอกให้ Cloud Firestore แมปกับรหัสเอกสาร เราจะอธิบายเรื่องนี้อย่างละเอียดยิ่งขึ้นด้านล่าง
  5. ใช้ documentReference.data(as: ) เพื่อจับคู่การอ้างอิงเอกสารกับ Swift ประเภท
  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 และ เพิ่มคำอธิบายประก��บด้วย Wrapper พร็อพเพอร์ตี้ @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: ทั้งหมดนี้ใช้งานได้กับโค้ดที่เรามี ที่เขียนในส่วนเริ่มต้น

อาร์เรย์

บางครั้งเราก็ต้องการจัดเก็บคอลเล็กชันของค่าไว้ในเอกสาร ประเภทของ หนังสือเป็นตัวอย่างที่ดี เช่น หนังสืออย่าง The Hitchhiker's Guide to the Galaxy อาจจัดอยู่ในหลายหมวดหมู่ ซึ่งในกรณีนี้คือ "ไซไฟ" และ "ตลก"

การจัดเก็บอาร์เรย์ในเอกสาร 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]
}

ข้อความสั้นๆ เกี่ยวกับการแมปรหัสเอกสาร

เราจะมาพูดถึงการแมป ID เอกสารกันก่อน ครู่หนึ่ง

เราใช้ Wrapper พร็อพเพอร์ตี้ @DocumentID ในตัวอย่างก่อนหน้านี้บางส่วน เพื่อแมปรหัสเอกสารของเอกสาร Cloud Firestore กับพร็อพเพอร์ตี้ id ของประเภท Swift ซึ่งมีความสำคัญเนื่องจากเหตุผลหลายประการ:

  • ซึ่งจะช่วยให้เราทราบว่าควรอัปเดตเอกสารใดในกรณีที่ผู้ใช้ระบุถึง การเปลี่ยนแปลง
  • List ของ SwiftUI กำหนดให้องค์ประกอบเป็น Identifiable เพื่อ เพื่อป้องกันไม่ให้องค์ประกอบเคลื่อนไหวเมื่อแทรก

โปรดทราบว่าแอตทริบิวต์ที่ทำเครื่องหมายเป็น @DocumentID จะไม่ เข้ารหัสโดยโปรแกรมเปลี่ยนไฟล์ของ Cloud Firestore เมื่อเขียนเอกสารกลับ นี่คือ เนื่องจาก ID เอกสารไม่ใช่แอตทริบิวต์ของตัวเอกสารเอง ดังนั้น การเขียนลงเอกสารนั้นถือเป็นความผิดพลาด

เมื่อทำงานกับประเภทที่ซ้อนกัน (เช่น อาร์เรย์ของแท็กใน 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 Wrapper พร็อพเพอร์ตี้นี้ถือเป็นเครื่องมือที่ทรงพลัง การจัดการกับการประทับเวลาในแอปของคุณ

ในระบบที่มีการกระจาย โอกาสที่นาฬิกาในแต่ละระบบ ไม่ซิงค์กันโดยสมบูรณ์ตลอดเวลา คุณอาจคิดว่านี่ไม่ใช่ แต่ให้จินตนาการถึงผลกระทบของนาฬิกาที่ไม่ซิงค์กันเล็กน้อยสำหรับ ระบบซื้อขายหุ้น: แม้แต่ค่าเบี่ยงเบนมิลลิวินาที ก็อาจทำให้เกิดความแตกต่าง หลายล้านดอลลาร์เมื่อดำเนินการซื้อขาย

Cloud Firestore จะจัดการแอตทริบิวต์ที่มีเครื่องหมาย @ServerTimestamp เป็น ติดตามดังนี้: หากแอตทริบิวต์คือ nil เมื่อคุณจัดเก็บ (โดยใช้ addDocument() สำหรับ เช่น Cloud Firestore จะกรอกข้อมูลในช่องนี้ด้วยเซิร์ฟเวอร์ปัจจุบัน ณ เวลานั้นขณะเขียนลงในฐานข้อมูล หากช่องนี้ไม่ใช่ nil เมื่อคุณเรียกใช้ addDocument() หรือ updateData() 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 และเราแมปได้ ระหว่าง 2 ประเภทนี้ด้วยการดำเนินการต่อไปนี้

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

หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการค้นหาเอกสารตามสถานที่ตั้งจริง โปรดดูที่ คู่มือการใช้โซลูชันนี้

Enum

Enum อาจเป็นฟีเจอร์ภาษาหนึ่งที่มีประสิทธิภาพต่ำที่สุดใน Swift มีอะไรหลายอย่างมากกว่าที่ตาเห็น กรณีการใช้งานทั่วไปสำหรับ enum คือ สร้างรูปแบบสถานะที่ แยกออกจากกันของสิ่งต่างๆ เช่น เราอาจเขียนแอป ในการจัดการบทความ หากต้องการติดตามสถานะของบทความ เราอาจต้องการใช้ enum Status:

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

Cloud Firestore ไม่รองรับ Enum ตั้งแต่ต้น (กล่าวคือไม่สามารถบังคับใช้ ชุดหนึ่ง) แต่เราสามารถใช้ประโยชน์จากข้อเท็จจริงที่ว่า enum สามารถพิมพ์ได้ และเลือกประเภทรหัสได้ ในตัวอย่างนี้ เราเลือก String ซึ่งหมายความว่า ค่า enum ทั้งหมดจะถูกแมปกับ/จากสตริงเมื่อจัดเก็บไว้ใน เอกสาร Cloud Firestore

และเนื่องจาก 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 ที่เราสามารถ เพิ่มลงในโครงสร้างโค้ดได้เพื่อระบุวิธีจับคู่แอตทริบิวต์บางรายการ

ลองพิจารณาเอกสารนี้:

เอกสาร Firestore ที่มีชื่อแอตทริบิวต์ snake_cased

ในการแมปเอกสารนี้กับ Struct ที่มีพร็อพเพอร์ตี้ชื่อประเภท String เรา ต้องเพิ่ม enum ของ CodingKeys ลงในโครงสร้าง ProgrammingLanguage และระบุ ชื่อแอตทริบิวต์ในเอกสาร:

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

โดยค่าเริ่มต้น Codable API จะใช้ชื่อพร็อพเพอร์ตี้ของประเภท Swift เพื่อ กำหนดชื่อแอตทริบิวต์ในเอกสาร Cloud Firestore ที่เราพยายามอยู่ ลงในแผนที่ ตราบใดที่ชื่อแอตทริบิวต์ตรงกัน ก็ไม่จำเป็นต้องเพิ่ม CodingKeys กับประเภทการเขียนโค้ดของเรา อย่างไรก็ตาม เมื่อเราใช้ CodingKeys สำหรับ ประเภทที่เฉพาะเจาะจง เราจำเป็นต้องเพิ่มชื่อพร็อพเพอร์ตี้ทั้งหมดที่เราต้องการทำแผนที่

ในข้อมูลโค้ดข้างต้น เราได้กำหนดพร็อพเพอร์ตี้ id ที่อาจต้องการ ใช้เป็นตัวระบุในมุมมอง SwiftUI List หากเราไม่ได้ระบุใน CodingKeys ก็จะไม่แมป เมื่อดึงข้อมูล จึงกลายเป็น nil ซึ่งจะส่งผลให้มุมมอง List ได้รับการเติมด้วยเอกสารแรก

พร็อพเพอร์ตี้ใดๆ ที่ไม่ได้ระบุเป็นเคสใน enum ของ CodingKeys ที่เกี่ยวข้อง จะถูกละเว้นในระหว่างกระบวนการแมป วิธีนี้สะดวกมากหาก เราจ��งต้องการยกเว้นบางที่พักไม่ให้ถูกทำแผนที่

ตัวอย่างเช่น หากเราต้องการยกเว้นพร็อพเพอร์ตี้ reasonWhyILoveThis จาก ที่ถูกแมป สิ่งที่เราต้องทำคือ นำออกจาก enum CodingKeys:

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

บางครั้งเราอาจต้องการเขียนแอตทริบิวต์ที่ว่างเปล่ากลับเข้าไปใน เอกสาร Cloud Firestore Swift มีแนวคิดเกี่ยวกับตัวเลือกเพื่อแสดงถึง ไม่มีค่า และ Cloud Firestore รองรับค่า null เช่นกัน อย่างไรก็ตาม ลักษณะการทำงานเริ่มต้นของตัวเลือกการเข้ารหัสที่มีค่า nil คือ ที่จะข้ามคำเหล่านั้นไป @ExplicitNull ทำให้เราสามารถควบคุมวิธีการที่ Swift ระบบจะจัดการกับตัวเลือกที่ไม่บังคับเมื่อเข้ารหัส โดยทำเครื่องหมายพร็อพเพอร์ตี้ที่ไม่บังคับว่า @ExplicitNull เราสามารถบอกให้ Cloud Firestore เขียนพร็อพเพอร์ตี้นี้ลงในส่วน เอกสารที่มีค่า Null หากมีค่า nil

การใช้โปรแกรมเปลี่ยนไฟล์และตัวถอดรหัสที่กำหนดเองสำหรับการจับคู่สี

หัวข้อสุดท้ายเกี่ยวกับความครอบคลุมของข้อมูลแผนที่ด้วย Codable มาดู โปรแกรมเปลี่ยนไฟล์และตัวถอดรหัสที่กำหนดเอง ส่วนนี้ไม่ครอบคลุมโฆษณาเนทีฟ ประเภทข้อมูล Cloud Firestore แต่โปรแกรมเปลี่ยนไฟล์และตัวถอดรหัสที่กำหนดเองมีประโยชน์อย่างยิ่ง ในแอป Cloud Firestore

"ฉันจะจับคู่สีได้อย่างไร" เป็นคำถามหนึ่งที่นักพัฒนาซอฟต์แวร์ถามบ่อย ไม่เพียงสำหรับ Cloud Firestore แต่สำหรับการแมประหว่าง Swift และ JSON เป็น มีโซลูชันมากมายให้ใช้งาน แต่ส่วนใหญ่มุ่งเน้นที่ JSON และเกือบทุกสีจะจับคู่สีเป็นพจนานุกรมที่ซ้อนกันซึ่งประกอบขึ้นเป็น RGB คอมโพเนนต์

ดูเหมือนว่าจะมีวิธีแก้ปัญหาที่ดีกว่าและง่ายกว่านี้ ทำไมเราถึงไม่ใช้สีของเว็บ (หรือถ้าจะให้เฉพาะเจาะจงมากขึ้นก็คือรูปแ��บสีเลขฐาน 16 ของ CSS) รูปแบบเหล่านี้ใช้งานง่าย (โดยพื้นฐานแล้วคือสตริง) และยังสนับสนุนความโปร่งใสอีกด้วย!

เพื่อให้จับคู่ Color ของ Swift กับค่าเลขฐานสิบหกได้ เราต้องสร้าง Swift ส่วนขยายที่เพิ่ม Codable ไปยัง Color

extension Color {

 init(hex: String) {
    let rgba = hex.toRGBA()

    self.init(.sRGB,
              red: Double(rgba.r),
              green: Double(rgba.g),
              blue: Double(rgba.b),
              opacity: Double(rgba.alpha))
    }

    //... (code for translating between hex and RGBA omitted for brevity)

}

extension Color: Codable {
  
  public init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let hex = try container.decode(String.self)

    self.init(hex: hex)
  }
  
  public func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(toHex)
  }

}

เมื่อใช้ decoder.singleValueContainer() เราจะสามารถถอดรหัส String ไปยัง เทียบเท่า Color โดยไม่ต้องฝังคอมโพเนนต์ RGBA นอกจากนี้ คุณยังสามารถ ใช้ค่าเหล่านี้ใน 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)"
        }
      }
    }
  }
}

การจัดการข้อผิดพลาดในการอัปเดตแบบเรียลไทม์

ข้อมูลโค้ดก่อนหน้านี้แสดงวิธีจัดการกับข้อผิดพลาดเมื่อดึงข้อมูล เอกสารเดียว นอกจากการดึงข้อมูล 1 ครั้งแล้ว Cloud Firestore ยัง รองรับการส่งอัปเดตของแอปทันทีที่เกิดขึ้น โดยใช้สแนปชอตที่เรียกกันว่า Listener: เราสามารถลงทะเบียน Listener ของสแนปชอตในคอลเล็กชัน (หรือข้อความค้นหา) และ Cloud Firestore จะโทรหา Listener ของเราทุกครั้งที่มีการอัปเดต

นี่คือข้อมูลโค้ดที่แสดงวิธีลงทะเบียน Listener สแนปชอต ข้อมูลแผนที่ โดยใช้ Codable และจัดการข้อผิดพลาดใดๆ ที่อาจเกิดขึ้น และยังแสดงวิธีเพิ่ม เอกสารใหม่ในคอลเล็กชัน จะเห็นว่าไม่มีความจำเป็นต้องอัปเดต อาร์เรย์ท้องถิ่นที่ถือเอกสารที่ทำแผนที่เอง เนื่องจากเราได้จัดการเรื่องนี้แล้ว ของโค้ดใน Listener ของสแนปชอต

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 ได้เลย!

Codable API ของ Swift เป็นวิธีที่มีประสิทธิภาพและยืดหยุ่นในการแมปข้อมูลจาก รูปแบบที่ต่อเนื่องกันไปยังและจากโมเดลข้อมูลแอปพลิเคชันของคุณ ในคู่มือนี้ คุณเห็นว่าใช้งานง่ายในแอปที่ใช้ Cloud Firestore เป็น เก็บข้อมูล

จากตัวอย่างพื้นฐานที่มีประเภทข้อมูลง่ายๆ เราค่อยๆ ช่วยเพิ่มความซับซ้อนของโมเดลข้อมูล ในขณะที่สามารถพึ่งพา การติดตั้งใช้งาน Codable และ Firebase เพื่อทำการแมปให้กับเรา

เราขอแนะนำแหล่งข้อมูลต่อไปนี้สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับ Codable

แม้ว่าเราจะพยายามอย่างสุดความสามารถในการรวบรวมคู่มือที่ครอบคลุมสำหรับการทำแผนที่ เอกสาร Cloud Firestore เป็นเพียงตัวอย่างบางส่วนเท่านั้น และคุณอาจกำลังใช้ กลยุทธ์อื่นๆ ในการจัดทำประเภทของคุณได้ ให้ใช้ปุ่มส่งความคิดเห็นด้านล่าง โปรดบอกให้เราทราบว่ากลยุทธ์ใดที่คุณใช้สำหรับการทำแผนที่ประเภทอื่นๆ ข้อมูล Cloud Firestore หรือแสดงข้อมูลใน Swift

ไม่มีเหตุผลที่จะไม่ใช้การรองร��บ Codable ของ Cloud Firestore