Codable API ของ Swift ซึ่งเปิดตัวใน Swift 4 ช่วยให้เราใช้ประโยชน์จากประสิทธิภาพของคอมไพเลอร์เพื่อแมปข้อมูลจากรูปแบบที่เรียงลำดับเป็นประเภท Swift ได้ง่ายขึ้น
คุณอาจเคยใช้ Codable เพื่อแมปข้อมูลจาก Web API กับโมเดลข้อมูลของแอป (และในทางกลับกัน) แต่ Codable มีความยืดหยุ่นมากกว่านั้นมาก
ในคู่มือนี้ เราจะมาดูวิธีใช้ Codable เพื่อแมปข้อมูลจาก Cloud Firestore กับประเภท Swift และในทางกลับกัน
เมื่อดึงข้อมูลเอกสารจาก Cloud Firestore แอปของคุณจะได้รับพจนานุกรมของคู่คีย์/ค่า (หรืออาร์เรย์ของพจนานุกรม หากคุณใช้การดำเนินการอย่างใดอย่างหนึ่งที่แสดงผลเอกสารหลายรายการ)
ตอนนี้คุณสามารถใช้พจนานุกรมใน Swift ต่อไปได้โดยตรง และพจนานุกรมเหล่านี้มีความยืดหยุ่นสูงซึ่งอาจเป็นสิ่งที่คุณต้องการในกรณีการใช้งานของคุณ อย่างไรก็ตาม วิธีนี้ไม่ปลอดภัยในการกำหนดประเภท และอาจทำให้เกิดข้อบกพร่องที่ติดตามได้ยากได้ง่ายๆ จากการสะกดชื่อแอตทริบิวต์ผิด หรือลืมแมปแอตทริบิวต์ใหม่ที่ทีมเพิ่มเข้ามาเมื่อเปิดตัวฟีเจอร์ใหม่ที่น่าตื่นเต้นเมื่อสัปดาห์ที่แล้ว
ในอดีต นักพัฒนาแอปหลายรายได้แก้ไขข้อบกพร่องเหล่านี้โดยใช้เลเยอร์การแมปอย่างง่ายที่ช่วยให้แมปพจนานุกรมกับประเภท 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 เป็นชื่อแทนประเภทสำหรับโปรโตคอล 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)")
}
การถอดรหัสออบเจ็กต์ 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 ระบบจะแปลง 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 การทำเครื่องหมายพร็อพเพอร์ตี้ cover ใน BookWithCoverImages เป็นไม่บังคับจะช่วยให้เราจัดการกับกรณีที่เอกสารบางรายการอาจไม่มีแอตทริบิวต์ cover ได้
หากสงสัยว่าเหตุใดจึงไม่มีข้อมูลโค้ดสำหรับการดึงข้อมูลหรืออัปเดตข้อมูล คุณจะยินดีที่ได้ทราบว่าไม่จำเป็นต้องปรับโค้ดสำหรับการอ่านหรือเขียนจาก/ไปยัง 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]
}
ข้อมูลสั้นๆ เกี่ยวกับการจับคู่รหัสเอกสาร
ก่อนที่เราจะไปต่อกับการแมปประเภทอื่นๆ เรามาพูดถึงการแมปรหัสเอกสารกันสักครู่
เราใช้ Wrapper พร็อพเพอร์ตี้ @DocumentID ในตัวอย่างก่อนหน้านี้บางรายการเพื่อแมปรหัสเอกสารของเอกสาร Cloud Firestore กับพร็อพเพอร์ตี้ id ของประเภท Swift ซึ่งเป็นสิ่งสำคัญเนื่องจากเหตุผลต่อไปนี้
- ซึ่งจะช่วยให้เราทราบว่าควรอัปเดตเอกสารใดในกรณีที่ผู้ใช้ทำการเปลี่ยนแปลงในเครื่อง
Listของ SwiftUI กำหนดให้องค์ประกอบต้องเป็นIdentifiableเพื่อป้องกันไม่ให้องค์ประกอบกระโดดไปมาเมื่อแทรก
โปรดทราบว่าแอตทริบิวต์ที่ทำเครื่องหมายเป็น @DocumentID จะไม่ได้รับการเข้ารหัสโดยโปรแกรมเปลี่ยนไฟล์ของ Cloud Firestore เมื่อเขียนเอกสารกลับ เนื่องจากรหัสเอกสารไม่ใช่แอตทริบิวต์ของเอกสารเอง ดังนั้นการเขียนรหัสลงในเอกสารจึงเป็นข้อผิดพลาด
เมื่อทำงานกับประเภทที่ฝัง (เช่น อาร์เรย์ของแท็กใน Book ในตัวอย่างก่อนหน้านี้ในคู่มือนี้) คุณไม่จำเป็นต้องเพิ่มพร็อพเพอร์ตี้ @DocumentID เนื่องจากพร็อพเพอร์ตี้ที่ฝังเป็นส่วนหนึ่งของเอกสาร Cloud Firestore และไม่ได้ประกอบกันเป็นเอกสารแยกต่างหาก ดังนั้นจึงไม่จำเป็นต้องมีรหัสเอกสาร
วันที่และเวลา
Cloud Firestore มีประเภทข้อมูลในตัวสำหรับจัดการวันที่และเวลา และการรองรับ Codable ของ Cloud Firestore ทำให้ใช้งานได้ง่าย
ลองดูเอกสารนี้ซึ่งแสดงถึงต้นกำเนิดของภาษาโปรแกรมทั้งหมดอย่าง Ada ที่คิดค้นขึ้นในปี 1843

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

หากต้องการแมปเอกสารนี้กับโครงสร้างที่มีพร็อพเพอร์ตี้ชื่อประเภท 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 มีแนวคิดเกี่ยวกับออบเจ็กต์ Optional เพื่อระบุว่าไม่มีค่า และ Cloud Firestore ก็รองรับค่า null ด้วยเช่นกัน
อย่างไรก็ตาม ลักษณะการทำงานเริ่มต้นสำหรับการเข้ารหัสตัวเลือกที่มีค่า nil คือการละเว้นตัวเลือกเหล่านั้น @ExplicitNull ช่วยให้เราควบคุมวิธีจัดการกับออบเจ็กต์ Optional ของ Swift เมื่อเข้ารหัสได้ โดยการแจ้งว่าพร็อพเพอร์ตี้ Optional เป็น @ExplicitNull เราจะบอกให้ Cloud Firestore เขียนพร็อพเพอร์ตี้นี้ลงในเอกสารที่มีค่า Null หากมีค่าเป็น nil
การใช้โปรแกรมเปลี่ยนไฟล์และโปรแกรมถอดรหัสที่กำหนดเองสำหรับการแมปสี
หัวข้อสุดท้ายในเรื่องการแมปข้อมูลด้วย Codable คือการแนะนำตัวเข้ารหัสและตัวถอดรหัสที่กำหนดเอง ส่วนนี้ไม่ได้ครอบคลุมประเภทข้อมูล Cloud Firestore แบบเนทีฟ แต่โปรแกรมเปลี่ยนไฟล์และโปรแกรมถอดรหัสที่กำหนดเองมีประโยชน์อย่างมากในแอป Cloud Firestore
"ฉันจะแมปสีได้อย่างไร" เป็นหนึ่งในคำถามที่นักพัฒนาแอปถามบ่อยที่สุด ไม่เพียงแต่สำหรับ Cloud Firestore เท่านั้น แต่ยังรวมถึงการแมประหว่าง Swift กับ JSON ด้วย มีโซลูชันมากมาย แต่ส่วนใหญ่จะเน้นที่ JSON และเกือบทั้งหมดจะแมปสีเป็นพจนานุกรมที่ซ้อนกันซึ่งประกอบด้วยคอมโพเนนต์ RGB
ดูเหมือนว่าน่าจะมีโซลูชันที่ดีกว่าและง่ายกว่านี้ ทำไมเราไม่ใช้สีเว็บ (หรือพูดให้เจาะจงกว่านั้นคือสัญกรณ์รหัสสีแบบเลขฐาน 16 ของ CSS) ซึ่งใช้งานง่าย (โดยพื้นฐานแล้วเป็นเพียงสตริง) และยังรองรับความโปร่งใสด้วย
หากต้องการแมป Swift Color กับค่าฐานสิบหก เราต้องสร้างส่วนขยาย Swift ที่เพิ่ม Codable ลงใน Color
extension Color {
init(hex: String) {
let rgba = hex.toRGBA()
self.init(.sRGB,
red: Double(rgba.r),
green: Double(rgba.g),
blue: Double(rgba.b),
opacity: Double(rgba.alpha))
}
//... (code for translating between hex and RGBA omitted for brevity)
}
extension Color: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let hex = try container.decode(String.self)
self.init(hex: hex)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(toHex)
}
}
เมื่อใช้ decoder.singleValueContainer() เราจะถอดรหัส String เป็น Color ที่เทียบเท่าได้โดยไม่ต้องฝังคอมโพเนนต์ RGBA นอกจากนี้ คุณยังใช้ค่าเหล่านี้ใน UI บนเว็บของแอปได้โดยไม่ต้องแปลงค่าก่อน
ด้วยวิธีนี้ เราสามารถอัปเดตโค้ดสำหรับการแมปแท็ก ซึ่งช่วยให้จัดการสีของแท็กได้ง่ายขึ้นโดยตรง แทนที่จะต้องแมปสีด้วยตนเองในโค้ด 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 ยังรองรับการส่งอัปเดตไปยังแอปของคุณเมื่อเกิดขึ้น โดยใช้ 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 เราขอแนะนำแหล่งข้อมูลต่อไปนี้
- John Sundell มีบทความดีๆ เกี่ยวกับพื้นฐานของ Codable
- หากคุณชอบอ่านหนังสือมากกว่า ลองดู Flight School Guide to Swift Codable ของ Mattt
- และสุดท้าย Donny Wals มีซีรีส์เกี่ยวกับ Codable ทั้งหมด
แม้ว่าเราจะพยายามอย่างเต็มที่เพื่อรวบรวมคู่มือที่ครอบคลุมสำหรับการแมปเอกสาร Cloud Firestore แต่คู่มือนี้ก็ยังไม่สมบูรณ์ และคุณอาจใช้กลยุทธ์อื่นๆ เพื่อแมปประเภทของคุณ โปรดใช้ปุ่มส่งความคิดเห็นด้านล่างเพื่อแจ้งให้เราทราบว่าคุณใช้กลยุทธ์ใดในการแมปข้อมูล Cloud Firestore ประเภทอื่นๆ หรือแสดงข้อมูลใน Swift
ไม่มีเหตุผลใดเลยที่จะไม่ใช้การรองรับ Codable ของ Cloud Firestore