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 ระบบจะแปลงเป็นสตริงและในทางกลับกัน
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 เนื่องจากทั้งหมดนี้ใช้ได้กับโค้ดที่เราเขียนไว้ในส่วนเริ่มต้น
อาร์เรย์
บางครั้งเราก็ต้องการจัดเก็บชุดค่าในเอกสาร ตัวอย่างที่ดีคือประเภทของหนังสือ เช่น หนังสืออย่างคู่มือพเนจรฉบับกู้โลก อาจจัดอยู่ในหลายหมวดหมู่ ในกรณีนี้คือ "นิยายวิทยาศาสตร์" และ "ตลก"
ใน 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
Enums อาจเป็นหนึ่งในฟีเจอร์ภาษาที่ถูกประเมินค่าต่ำที่สุดใน Swift
เพราะมีอะไรมากกว่าที่เห็น กรณีการใช้งานทั่วไปสำหรับ Enum คือการ
สร้างโมเดลสถานะที่ไม่ต่อเนื่องของสิ่งหนึ่งๆ เช่น เราอาจเขียนแอป
สำหรับจัดการบทความ หากต้องการติดตามสถานะของบทความ เราอาจต้องใช้
enum Status
enum Status: String, Codable {
case draft
case inReview
case approved
case published
}
Cloud Firestore ไม่รองรับการแจงนับโดยค่าเริ่มต้น (กล่าวคือ บังคับใช้ชุดค่าไม่ได้) แต่เรายังคงใช้ประโยชน์จากข้อเท็จจริงที่ว่าการแจงนับสามารถพิมพ์ได้
และเลือกประเภทที่เข้ารหัสได้ ในตัวอย่างนี้ เราเลือก String
ซึ่งหมายความว่า
ค่า enum ทั้งหมดจะได้รับการแมปเป็นสตริงเมื่อจัดเก็บในเอกสาร Cloud Firestore
และเนื่องจาก Swift รองรับค่าดิบที่กำหนดเอง เราจึงปรับแต่งค่าที่อ้างอิงถึงเคสของ Enum ได้ด้วย
เช่น หากเราตัดสินใจจัดเก็บเคส
Status.inReview
เป็น "อยู่ระหว่างตรวจสอบ" เราก็เพียงอัปเดตการแจงนับข้างต้นดังนี้
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
ลงในโครงสร้าง 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
ระบบจะไม่แมปเมื่อดึงข้อมูล และจะกลายเป็น nil
ซึ่งจะส่งผลให้มุมมอง List
เต็มไปด้วยเอกสารแรก
ระบบจะไม่สนใจพร็อพเพอร์ตี้ที่ไม่ได้แสดงเป็นเคสใน CodingKeys
enum
ที่เกี่ยวข้องในระหว่างกระบวนการแมป ซึ่งอาจสะดวกในกรณีที่เราต้องการยกเว้นที่พักบางแห่งไม่ให้แมป
เช่น หากต้องการยกเว้นพร็อพเพอร์ตี้ 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
ช่วยให้เราควบคุมวิธีจัดการตัวเลือก Swift
เมื่อเข้ารหัสได้ โดยการติดค่าสถานะพร็อพเพอร์ตี้ที่ไม่บังคับเป็น
@ExplicitNull
เราจะบอก Cloud Firestore ให้เขียนพร็อพเพอร์ตี้นี้ลงใน
เอกสารที่มีค่าเป็น Null หากมีค่าเป็น nil
การใช้โปรแกรมเปลี่ยนไฟล์และโปรแกรมถอดรหัสที่กำหนดเองสำหรับการแมปสี
ในหัวข้อสุดท้ายของการครอบคลุมเรื่องการแมปข้อมูลด้วย Codable เรามาทำความรู้จัก ตัวเข้ารหัสและตัวถอดรหัสที่กำหนดเองกัน ส่วนนี้ไม่ได้ครอบคลุมCloud Firestoreประเภทข้อมูลดั้งเดิม แต่ตัวเข้ารหัสและตัวถอดรหัสที่กำหนดเองมีประโยชน์อย่างมากในCloud Firestoreแอปของคุณ
"ฉันจะแมปสีได้อย่างไร" เป็นหนึ่งในคำถามที่นักพัฒนาแอปถามบ่อยที่สุด ไม่เพียงแต่สำหรับ Cloud Firestore แต่ยังรวมถึงการแมประหว่าง Swift กับ JSON ด้วย มีโซลูชันมากมาย แต่ส่วนใหญ่มุ่งเน้นไปที่ JSON และเกือบทั้งหมดจะแมปสีเป็นพจนานุกรมที่ซ้อนกันซึ่งประกอบด้วยคอมโพเนนต์ RGB
ดูเหมือนว่าควรจะมีโซลูชันที่ดีกว่าและง่ายกว่านี้ ทำไมเราจึงไม่ใช้สีเว็บ (หรือพูดให้เจาะจงมากขึ้นคือสัญกรณ์สีเลขฐานสิบหกของ 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 ยัง รองรับการส่งการอัปเดตไปยังแอปของคุณเมื่อมีการอัปเดต โดยใช้สิ่งที่เรียกว่าเครื่องมือฟัง สแนปชอต ซึ่งเราสามารถลงทะเบียนเครื่องมือฟังสแนปชอตในคอลเล็กชัน (หรือการค้นหา) และ Cloud Firestore จะเรียกเครื่องมือฟังของเราเมื่อใดก็ตามที่มีการอัปเดต
ต่อไปนี้คือข้อมูลโค้ดที่แสดงวิธีลงทะเบียนเครื่องมือฟังสแนปชอต แมปข้อมูล โดยใช้ Codable และจัดการข้อผิดพลาดที่อาจเกิดขึ้น นอกจากนี้ยังแสดงวิธีเพิ่ม เอกสารใหม่ลงในคอลเล็กชันด้วย ดังที่คุณจะเห็นว่าไม่จำเป็นต้องอัปเดตอาร์เรย์ในเครื่องที่เก็บเอกสารที่แมปไว้ด้วยตนเอง เนื่องจากโค้ดในเครื่องมือฟังสแนปชอตจะจัดการให้
class MappingColorsViewModel: ObservableObject {
@Published var colorEntries = [ColorEntry]()
@Published var newColor = ColorEntry.empty
@Published var errorMessage: String?
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
public func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}
func subscribe() {
if listenerRegistration == nil {
listenerRegistration = db.collection("colors")
.addSnapshotListener { [weak self] (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
self?.errorMessage = "No documents in 'colors' collection"
return
}
self?.colorEntries = documents.compactMap { queryDocumentSnapshot in
let result = Result { try queryDocumentSnapshot.data(as: ColorEntry.self) }
switch result {
case .success(let colorEntry):
if let colorEntry = colorEntry {
// A ColorEntry value was successfully initialized from the DocumentSnapshot.
self?.errorMessage = nil
return colorEntry
}
else {
// A nil value was successfully initialized from the DocumentSnapshot,
// or the DocumentSnapshot was nil.
self?.errorMessage = "Document doesn't exist."
return nil
}
case .failure(let error):
// A ColorEntry value could not be initialized from the DocumentSnapshot.
switch error {
case DecodingError.typeMismatch(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.valueNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.keyNotFound(_, let context):
self?.errorMessage = "\(error.localizedDescription): \(context.debugDescription)"
case DecodingError.dataCorrupted(let key):
self?.errorMessage = "\(error.localizedDescription): \(key)"
default:
self?.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
return nil
}
}
}
}
}
func addColorEntry() {
let collectionRef = db.collection("colors")
do {
let newDocReference = try collectionRef.addDocument(from: newColor)
print("ColorEntry stored with new document reference: \(newDocReference)")
}
catch {
print(error)
}
}
}
โค้ดที่ใช้ในโพสต์นี้เป็นส่วนหนึ่งของแอปพลิเคชันตัวอย่างที่คุณดาวน์โหลดได้จากที่เก็บ GitHub นี้
ไปใช้ Codable กันเลย
Codable API ของ Swift เป็นวิธีที่มีประสิทธิภาพและยืดหยุ่นในการแมปข้อมูลจากรูปแบบที่ซีเรียลไลซ์ไปยังและจากโมเดลข้อมูลของแอปพลิเคชัน ในคู่มือนี้ คุณได้เห็นแล้วว่าการใช้งานในแอปที่ใช้ Cloud Firestore เป็น ที่เก็บข้อมูลนั้นง่ายเพียงใด
เริ่มจากตัวอย่างพื้นฐานที่มีประเภทข้อมูลอย่างง่าย เราค่อยๆ เพิ่มความซับซ้อนของโมเดลข้อมูลไปเรื่อยๆ โดยที่ยังคงใช้ Codable และการใช้งานของ Firebase เพื่อทำการแมปให้เราได้
ดูรายละเอียดเพิ่มเติมเกี่ยวกับ Codable ได้จากแหล่งข้อมูลต่อไปนี้
- John Sundell มีบทความดีๆ เกี่ยวกับพื้นฐานของ Codable
- หากคุณชอบอ่านหนังสือมากกว่า ลองดูคำแนะนำเกี่ยวกับ Swift Codable จาก Flight School ของ Mattt
- และสุดท้าย Donny Wals มีซีรีส์ทั้งหมดเกี่ยวกับ Codable
แม้ว่าเราจะพยายามอย่างเต็มที่ในการรวบรวมคำแนะนำที่ครอบคลุมสำหรับการแมปเอกสารCloud Firestore แต่คำแนะนำนี้ก็ยังไม่สมบูรณ์ และคุณอาจใช้กลยุทธ์อื่นๆ ในการแมปประเภท โปรดแจ้งให้เราทราบว่าคุณใช้กลยุทธ์ใดในการแมปข้อมูลประเภทอื่นๆ ของ Cloud Firestore หรือการแสดงข้อมูลใน Swift โดยใช้ปุ่มส่งความคิดเห็นด้านล่าง
ไม่มีเหตุผลใดเลยที่จะไม่ใช้การรองรับ Codable ของ Cloud Firestore