Codable API ของ Swift ซึ่งเปิดตัวใน Swift 4 ช่วยให้เราสามารถใช้ประโยชน์จากความสามารถของคอมไพเลอร์เพื่อให้จับคู่ข้อมูลจากรูปแบบที่แปลงเป็นอนุกรมไปยังประเภท Swift ได้ง่ายขึ้น
คุณอาจเคยใช้ Codable เพื่อแมปข้อมูลจาก Web API กับรูปแบบข้อมูลของแอป (และในทางกลับกัน) แต่ Codable มีความยืดหยุ่นมากกว่านั้นมาก
ในคู่มือนี้ เราจะดูวิธีใช้ Codable เพื่อจับคู่ข้อมูลจาก Cloud Firestore กับประเภท Swift และในทางกลับกัน
เมื่อดึงข้อมูลเอกสารจาก Cloud Firestore แอปของคุณจะได้รับพจนานุกรมของคู่คีย์/ค่า (หรืออาร์เรย์ของพจนานุกรม หากคุณใช้การดำเนินการใดการดำเนินการหนึ่งซึ่งแสดงผลเอกสารหลายรายการ)
ตอนนี้คุณยังคงใช้พจนานุกรมใน Swift ได้โดยตรง และพจนานุกรมเหล่านี้ยังมอบความยืดหยุ่นที่ยอดเยี่ยมซึ่งอาจตรงกับกรณีการใช้งานของคุณ อย่างไรก็ตาม แนวทางนี้ไม่ปลอดภัยต่อประเภทและอาจทำให้เกิดข้อบกพร่องที่ติดตามได้ยากจากการสะกดชื่อแอตทริบิวต์ผิด หรือลืมจับคู่แอตทริบิวต์ใหม่ที่ทีมเพิ่มเมื่อเปิดตัวฟีเจอร์ใหม่ที่น่าตื่นเต้นเมื่อสัปดาห์ที่แล้ว
ที่ผ่านมา นักพัฒนาแอปจํานวนมากแก้ปัญหาเหล่านี้ด้วยการใช้เลเยอร์การแมปแบบง่ายที่ช่วยให้แมปพจนานุกรมกับประเภท Swift ได้ แต่อีกครั้ง การใช้งานส่วนใหญ่เหล่านี้จะอิงตามการกำหนดการแมประหว่างเอกสาร Cloud Firestore กับโมเดลข้อมูลประเภทที่เกี่ยวข้องของแอปด้วยตนเอง
Cloud Firestore รองรับ Codable API ของ 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
โดยไม่ได้ตั้งใจ ซึ่งจะทําให้เกิดปัญหาการแมปที่พบได้ยาก นอกจากนี้ คุณจะต้องอัปเดตโค้ดการแมปทุกครั้งที่มีการเพิ่มฟิลด์ใหม่ ซึ่งค่อนข้างยุ่งยาก
และอย่าลืมว่าเราไม่ได้ใช้ประโยชน์จากระบบประเภทที่เข้มงวดของ Swift ซึ่งจะรู้ประเภทที่ถูกต้องสำหรับพร็อพเพอร์ตี้แต่ละรายการของ Book
Codable คืออะไร
ตามเอกสารประกอบของ Apple ระบุว่า Codable คือ "ประเภทที่แปลงตัวเองเป็นการแสดงภายนอกและจากการแสดงภายนอกได้" ที่จริงแล้ว Codable คือตัวแปรแทนประเภทสำหรับโปรโตคอลที่โค้ดได้และถอดรหัสได้ การทำให้ประเภท 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)")
}
การถอดรหัสออบเจ็กต์ 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 ให้ทำตามขั้นตอนต่อไปนี้
- ตรวจสอบว่าคุณได้เพิ่มเฟรมเวิร์ก
FirebaseFirestore
ลงในโปรเจ็กต์แล้ว คุณใช้Swift Package Manager หรือ CocoaPods ก็ได้ - นําเข้า
FirebaseFirestore
ไปยังไฟล์ Swift - ปรับประเภทให้เป็นไปตาม
Codable
- (ไม่บังคับ หากต้องการใช้ประเภทในมุมมอง
List
) เพิ่มพร็อพเพอร์ตี้id
ลงในประเภท แล้วใช้@DocumentID
เพื่อบอก Cloud Firestore ให้แมปข้อมูลนี้กับรหัสเอกสาร เราจะอธิบายเรื่องนี้อย่างละเอียดด้านล่าง - ใช้
documentReference.data(as: )
เพื่อแมปการอ้างอิงเอกสารกับประเภท Swift - ใช้
documentReference.setData(from: )
เพื่อจับคู่ข้อมูลจากประเภท Swift กับเอกสาร Cloud Firestore - (ไม่บังคับแต่แนะนำอย่างยิ่ง) ใช้การจัดการข้อผิดพลาดที่เหมาะสม
มาอัปเดตประเภท 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 คือการใช้แผนที่ โดยทำดังนี้
เมื่อเขียนสตรูค 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?
}
โปรดสังเกตว่าเรากำหนด Struct CoverImages
สำหรับแผนที่ภาพปกในเอกสาร Cloud Firestore อย่างไร การทำเครื่องหมายพร็อพเพอร์ตี้หน้าปกในBookWithCoverImages
เป็น "ไม่บังคับ" จะช่วยให้เราจัดการกับความจริงที่ว่าเอกสารบางรายการอาจไม่มีแอตทริบิวต์หน้าปกได้
หากสงสัยว่าทำไมไม่มีข้อมูลโค้ดสําหรับการดึงข้อมูลหรืออัปเดตข้อมูล คุณก็ไม่ต้องกังวลไป เนื่องจากไม่ต้องปรับโค้ดสําหรับการอ่านหรือเขียนจาก/ไปยัง Cloud 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]
}
เนื่องจากการดำเนินการนี้ใช้ได้กับประเภทใดก็ได้ที่โค้ดได้ เราจึงใช้ประเภทที่กำหนดเองได้ด้วย ลองจินตนาการว่าเราต้องการจัดเก็บรายการแท็กสำหรับหนังสือแต่ละเล่ม นอกจากชื่อแท็กแล้ว เราต้องการจัดเก็บสีของแท็กด้วย ดังนี้
หากต้องการจัดเก็บแท็กด้วยวิธีนี้ สิ่งที่ต้องทำมีเพียงใช้โครงสร้าง 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
property wrapper ในตัวอย่างก่อนหน้านี้เพื่อจับคู่รหัสเอกสารของเอกสาร Cloud Firestore กับพร็อพเพอร์ตี้ id
ของประเภท Swift ซึ่งสำคัญอย่างยิ่งเนื่องจากเหตุผลต่อไปนี้
- ข้อมูลนี้ช่วยให้เราทราบว่าควรอัปเดตเอกสารใดในกรณีที่ผู้ใช้ทำการเปลี่ยนแปลงในเครื่อง
List
ของ SwiftUI กําหนดให้องค์ประกอบเป็นIdentifiable
เพื่อไม่ให้องค์ประกอบกระโดดไปมาเมื่อมีการแทรก
โปรดทราบว่าแอตทริบิวต์ที่ทำเครื่องหมายเป็น @DocumentID
จะไม่ได้รับการเข้ารหัสโดยโปรแกรมเข้ารหัสของ Cloud Firestore เมื่อเขียนเอกสารกลับ เนื่องจากรหัสเอกสารไม่ใช่แอตทริบิวต์ของเอกสารเอง การเขียนรหัสลงในเอกสารจึงเป็นข้อผิดพลาด
เมื่อทำงานกับประเภทที่ฝัง (เช่น อาร์เรย์ของแท็กใน Book
ในตัวอย่างก่อนหน้านี้ในคู่มือนี้) คุณไม่จําเป็นต้องเพิ่มพร็อพเพอร์ตี้ @DocumentID
เนื่องจากพร็อพเพอร์ตี้ที่ฝังเป็นส่วนหนึ่งของเอกสาร Cloud Firestore และไม่ใช่เอกสารแยกต่างหาก จึงไม่จำเป็นต้องมีรหัสเอกสาร
วันที่และเวลา
Cloud Firestore มีประเภทข้อมูลในตัวสำหรับจัดการวันที่และเวลา และเนื่องจาก Cloud Firestore รองรับ Codable คุณจึงใช้ข้อมูลประเภทดังกล่าวได้ง่ายๆ
มาดูเอกสารนี้กัน ซึ่งเป็นตัวแทนของภาษาโปรแกรมแม่ของทุกภาษาอย่าง Ada ซึ่งคิดค้นขึ้นในปี 1843
ประเภท Swift สำหรับการแมปเอกสารนี้อาจมีลักษณะดังนี้
struct ProgrammingLanguage: Codable {
@DocumentID var id: String?
var name: String
var year: Date
}
เราไม่สามารถออกจากส่วนนี้เกี่ยวกับวันที่และเวลาได้โดยไม่ต้องสนทนาเกี่ยวกับ @ServerTimestamp
ไวลด์การ์ดพร็อพเพอร์ตี้นี้มีประสิทธิภาพสูงเมื่อต้องจัดการกับการประทับเวลาในแอป
ในระบบแบบกระจาย นาฬิกาในแต่ละระบบอาจไม่ได้ซิงค์กันทั้งหมดตลอดเวลา คุณอาจคิดว่านี่ไม่ใช่เรื่องใหญ่ แต่ลองนึกถึงผลกระทบของนาฬิกาที่ทำงานไม่ตรงกันเล็กน้อยสำหรับระบบการซื้อขายหุ้น แม้แต่ความคลาดเคลื่อนเพียงไม่กี่มิลลิวินาทีก็อาจส่งผลให้เกิดความแตกต่างหลายล้านดอลลาร์เมื่อทำการซื้อขาย
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)
ดูข้อมูลเพิ่มเติมเกี่ยวกับการค้นหาเอกสารตามสถานที่ตั้งจริงได้ที่คู่มือโซลูชันนี้
Enums
อาจเป็นฟีเจอร์ภาษาที่ไม่ค่อยได้รับคำชื่นชมมากนักใน Swift เพราะมีความสามารถมากกว่าที่เห็น กรณีการใช้งานที่พบบ่อยสำหรับลิสต์แบบจำกัดคือการสร้างโมเดลสถานะแบบไม่ต่อเนื่องของสิ่งหนึ่ง ตัวอย่างเช่น เราอาจเขียนแอปสำหรับจัดการบทความ หากต้องการติดตามสถานะของบทความ เราอาจต้องใช้ 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 ที่เราสามารถเพิ่มลงในโครงสร้างที่โค้ดได้เพื่อระบุวิธีแมปแอตทริบิวต์บางอย่าง
พิจารณาเอกสารนี้
หากต้องการแมปเอกสารนี้กับโครงสร้างที่มีพร็อพเพอร์ตี้ชื่อประเภท String
เราจำเป็นต้องเพิ่ม CodingKeys
enum ลงในโครงสร้าง 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
ระบบจะไม่แมปเมื่อดึงข้อมูล และ CodingKeys
จะกลายเป็น nil
ซึ่งจะทำให้มุมมอง List
แสดงเอกสารฉบับแรก
ระบบจะไม่สนใจพร็อพเพอร์ตี้ที่ไม่ได้แสดงเป็นเคสใน CodingKeys
enum
ที่เกี่ยวข้องในระหว่างกระบวนการแมป ซึ่งวิธีนี้อาจสะดวกกว่าหากเราต้องยกเว้นที่พักบางแห่งไม่ให้แมป
ตัวอย่างเช่น หากต้องการยกเว้นการแมปพร็อพเพอร์ตี้ 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 Swift มีแนวคิดเกี่ยวกับค่าที่ไม่บังคับเพื่อบ่งบอกถึงการไม่มีค่า และ Cloud Firestore รองรับค่า null
ด้วย
อย่างไรก็ตาม ลักษณะการทำงานเริ่มต้นสำหรับการเข้ารหัสตัวเลือกที่มีค่า nil
คือจะละเว้นตัวเลือกเหล่านั้น @ExplicitNull
ช่วยให้เราควบคุมวิธีจัดการตัวเลือกของ Swift เมื่อเข้ารหัสได้ โดยการทำเครื่องหมายพร็อพเพอร์ตี้ที่ไม่บังคับเป็น @ExplicitNull
จะช่วยให้เราบอก Cloud Firestore ให้เขียนพร็อพเพอร์ตี้นี้ลงในเอกสารด้วยค่า Null ได้หากมีค่าเป็น nil
การใช้โปรแกรมเปลี่ยนไฟล์และโปรแกรมถอดรหัสที่กำหนดเองสำหรับการแมปสี
หัวข้อสุดท้ายในการครอบคลุมการแมปข้อมูลด้วย Codable คือการแนะนำโปรแกรมเข้ารหัสและโปรแกรมถอดรหัสที่กำหนดเอง ส่วนนี้ไม่ครอบคลุมถึงประเภทข้อมูล Cloud Firestore ดั้งเดิม แต่โปรแกรมเข้ารหัสและโปรแกรมถอดรหัสที่กำหนดเองมีประโยชน์อย่างมากในแอป Cloud Firestore
"ฉันจะแมปสีได้อย่างไร" เป็นหนึ่งในคำถามที่นักพัฒนาแอปถามบ่อยที่สุด ไม่ใช่แค่สำหรับ Cloud Firestore เท่านั้น แต่ยังรวมถึงการแมประหว่าง Swift กับ JSON ด้วย โซลูชันมีมากมาย แต่ส่วนใหญ่มุ่งเน้นที่ JSON และเกือบทั้งหมดแมปสีเป็นพจนานุกรมที่ฝังอยู่ซึ่งประกอบด้วยคอมโพเนนต์ RGB
ดูเหมือนว่าควรมีวิธีแก้ปัญหาที่ดีกว่าและง่ายกว่านี้ ทำไมเราไม่ใช้สีเว็บ (หรือเรียกให้เจาะจงกว่านั้นคือการใช้การเขียนรหัสสีแบบเลขฐานสิบหกของ CSS) เพราะสีเว็บนั้นใช้งานง่าย (โดยพื้นฐานแล้วเป็นเพียงสตริง) และรองรับความโปร่งใสด้วย
หากต้องการแมป Color
ของ Swift กับค่าฐาน 16 เราจะต้องสร้างส่วนขยาย 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)"
}
}
}
}
}
การจัดการข้อผิดพลาดในการอัปเดตแบบเรียลไทม์
ข้อมูลโค้ดก่อนหน้าแสดงวิธีจัดการข้อผิดพลาดเมื่อดึงข้อมูลเอกสารรายการเดียว นอกจากการดึงข้อมูลเพียงครั้งเดียวแล้ว Cloud Firestore ยังรองรับการนำข้อมูลอัปเดตไปยังแอปของคุณเมื่อเกิดข้อมูลอัปเดตขึ้นด้วย โดยใช้สิ่งที่เรียกว่า "โปรแกรมฟังสแนปชอต" ซึ่งเราลงทะเบียนโปรแกรมฟังสแนปชอตในคอลเล็กชัน (หรือคําค้นหา) ได้ และ Cloud Firestore จะเรียกใช้โปรแกรมฟังทุกครั้งที่มีข้อมูลอัปเดต
ด้านล่างนี้คือข้อมูลโค้ดที่แสดงวิธีลงทะเบียนโปรแกรมรับฟังสแนปชอต แมปข้อมูลโดยใช้ 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 ในแอปที่ใช้ Cloud Firestore เป็นดาต้าสตोरนั้นง่ายเพียงใด
เราเริ่มต้นจากตัวอย่างพื้นฐานที่มีประเภทข้อมูลง่ายๆ แล้วค่อยๆ เพิ่มความซับซ้อนของโมเดลข้อมูลไปเรื่อยๆ โดยอาศัย Codable และการใช้งาน Firebase เพื่อทำแมปให้เรา
ดูรายละเอียดเพิ่มเติมเกี่ยวกับ Codable ได้ที่แหล่งข้อมูลต่อไปนี้
- John Sundell มีบทความที่ยอดเยี่ยมเกี่ยวกับพื้นฐานของ Codable
- หากชอบอ่านหนังสือมากกว่า โปรดดูคู่มือ Flight School สำหรับ Swift Codable ของ Mattt
- และสุดท้าย Donny Wals มีชุดวิดีโอทั้งหมดเกี่ยวกับ Codable
แม้ว่าเราจะพยายามอย่างเต็มที่เพื่อรวบรวมคู่มือที่ครอบคลุมสำหรับการแมปเอกสาร Cloud Firestore แต่คู่มือนี้ก็ไม่ได้ครอบคลุมทั้งหมด และคุณอาจใช้กลยุทธ์อื่นๆ เพื่อแมปประเภทเอกสาร โปรดแจ้งให้เราทราบถึงกลยุทธ์ที่คุณใช้สำหรับการแมปข้อมูลCloud Firestoreประเภทอื่นๆ หรือการแสดงข้อมูลใน Swift โดยใช้ปุ่มส่งความคิดเห็นด้านล่าง
คุณไม่มีเหตุผลใดๆ ที่จะไม่ใช้การสนับสนุนแบบโค้ดได้ของ Cloud Firestore