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?
}
โปรดสังเกตวิธีที่เรากำหนดโครงสร้าง CoverImages
สำหรับแผนที่หน้าปกในเอกสาร Cloud Firestore การทำเครื่องหมายพร็อพเพอร์ตี้หน้าปกใน BookWithCoverImages
เป็น "ไม่บังคับ" ช่วยให้เรารับมือกับข้อเท็จจริงที่ว่าเอกสารบางฉบับไม่มีแอตทริบิวต์ปกได้
หากสงสัยว่าทำไมไม่มีข้อมูลโค้ดสําหรับการดึงข้อมูลหรืออัปเดตข้อมูล คุณก็ไม่ต้องกังวลไป เนื่องจากไม่ต้องปรับโค้ดสําหรับการอ่านหรือเขียนจาก/ไปยัง Cloud Firestore: ทั้งหมดนี้ทํางานกับโค้ดที่เราเขียนไว้ในส่วนแรก
อาร์เรย์
บางครั้งเราต้องการจัดเก็บคอลเล็กชันค่าในเอกสาร ประเภทของหนังสือเป็นตัวอย่างที่ดี ตัวอย่างหนังสืออย่าง The Hitchhiker's Guide to the Galaxy อาจจัดอยู่ในหลายหมวดหมู่ เช่น "ไซไฟ" และ "ตลก" เช่น
ใน 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
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
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)
ดูข้อมูลเพิ่มเติมเกี่ยวกับการค้นหาเอกสารตามสถานที่ตั้งจริงได้ในคู่มือโซลูชันนี้
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
เราต้องเพิ่ม 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
ซึ่งเราอาจต้องการใช้เป็นตัวระบุในมุมมอง List
ของ SwiftUI หากเราไม่ได้ระบุ ใน CodingKeys
ระบบจะไม่แมป URL ดังกล่าวเมื่อดึงข้อมูล จึงกลายเป็น 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 เป็นพื้นที่เก็บข้อมูล
เราเริ่มต้นจากตัวอย่างพื้นฐานที่มีประเภทข้อมูลง่ายๆ แล้วค่อยๆ เพิ่มความซับซ้อนของโมเดลข้อมูลไปเรื่อยๆ โดยอาศัย Codable และการใช้งาน Firebase เพื่อทำแมปให้เรา
เราขอแนะนำแหล่งข้อมูลต่อไปนี้สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับ Codable
- John Sundell มีบทความดีๆ เกี่ยวกับ Basics of Codable
- หากสนใจเกี่ยวกับหนังสือมากกว่า ให้ไปที่ Flight School Guide to Swift Codable ของ Mattt
- และสุดท้าย Donny Wals มีซีรีส์เกี่ยวกับ Codable ทั้งซีรีส์
แม้ว่าเราจะพยายามอย่างสุดความสามารถเพื่อรวบรวมคู่มือที่ครอบคลุมสำหรับการแมปเอกสาร Cloud Firestore รายการ แต่ก็ไม่ได้ครอบคลุมทั้งหมด และคุณอาจใช้กลยุทธ์อื่นๆ เพื่อแมปประเภทของคุณ โปรดแจ้งให้เราทราบถึงกลยุทธ์ที่คุณใช้สำหรับการแมปข้อมูลประเภทอื่นๆ ของ Cloud Firestore หรือการแสดงข้อมูลใน Swift โดยใช้ปุ่มส่งความคิดเห็นด้านล่าง
คุณไม่มีเหตุผลใดๆ ที่จะไม่ใช้การสนับสนุนแบบโค้ดได้ของ Cloud Firestore