Codable API ของ Swift ซึ่งเปิดตัวใน Swift 4 ช่วยให้เราสามารถใช้ประโยชน์จากประสิทธิภาพของคอมไพเลอร์เพื่อทำให้แมปข้อมูลจากรูปแบบซีเรียลไลซ์เป็นประเภท Swift ได้ง่ายขึ้น
คุณอาจใช้ Codable เพื่อจับคู่ข้อมูลจาก Web API กับโมเดลข้อมูลของแอปของคุณ (และในทางกลับกัน) แต่มีความยืดหยุ่นมากกว่านั้นมาก
ในคู่มือนี้ เราจะดูว่า 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 เป็นนามแฝงประเภทสำหรับโปรโตคอล 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
โดยใช้โค้ดเอเบิล
Cloud Firestore รองรับชุดข้อมูลที่หลากหลาย ตั้งแต่สตริงธรรมดาไปจนถึงแผนที่แบบซ้อน สิ่งเหล่านี้ส่วนใหญ่สอดคล้องโดยตรงกับประเภทที่มาพร้อมกับ Swift มาดูการแมปประเภทข้อมูลง่ายๆ ก่อนที่เราจะเริ่มเจาะลึกประเภทข้อมูลที่ซับซ้อนมากขึ้น
หากต้องการแมปเอกสาร Cloud Firestore กับประเภท Swift ให้ทำตามขั้นตอนเหล่านี้:
- ตรวจสอบให้แน่ใจว่าคุณได้เพิ่มเฟรมเวิร์ก
FirebaseFirestore
ให้กับโปรเจ็กต์ของคุณแล้ว คุณสามารถใช้ Swift Package Manager หรือ CocoaPods เพื่อดำเนินการดังกล่าวได้ - นำเข้า
FirebaseFirestore
ลงในไฟล์ Swift ของคุณ - ปรับประเภทของคุณให้เป็น
Codable
- (ไม่บังคับ หากคุณต้องการใช้ประเภทในมุมมอง
List
) เพิ่มคุณสมบัติid
ให้กับประเภทของคุณ และใช้@DocumentID
เพื่อบอก Cloud Firestore ให้แมปสิ่งนี้กับ ID เอกสาร เราจะพูดถึงเรื่องนี้โดยละเอียดด้านล่าง - ใช้
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 จะดูแลการกำหนด ID เอกสารใหม่ให้กับเอกสารโดยอัตโนมัติ ใช้งานได้แม้ในขณะที่แอปออฟไลน์อยู่
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 อาจแบ่งออกเป็นหลายประเภท ในกรณีนี้คือ "Sci-Fi" และ "Comedy":
ใน 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]
}
ข้อมูลสั้นๆ เกี่ยวกับการแมป ID เอกสาร
ก่อนที่เราจะไปยังการแมปประเภทอื่นๆ เรามาพูดถึงการแมป ID เอกสารกันสักครู่
เราใช้ wrapper คุณสมบัติ @DocumentID
ในตัวอย่างก่อนหน้านี้บางส่วนเพื่อจับคู่ ID เอกสารของเอกสาร 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
และเราสามารถแมประหว่างสองประเภทนั้นด้วยการดำเนินการต่อไปนี้:
CLLocationCoordinate2D(latitude: office.location.latitude,
longitude: office.location.longitude)
หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการสืบค้นเอกสารตามสถานที่ตั้ง โปรดดู คู่มือวิธีแก้ปัญหานี้
เอนัม
Enums อาจเป็นหนึ่งในคุณสมบัติภาษาที่ประเมินต่ำที่สุดใน 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 รองรับค่าดิบแบบกำหนดเอง เราจึงสามารถกำหนดได้ว่าค่าใดจะอ้างอิงถึงตัวพิมพ์แจงนับใด ตัวอย่างเช่น หากเราตัดสินใจจัดเก็บกรณี Status.inReview
เป็น "อยู่ระหว่างการตรวจสอบ" เราก็สามารถอัปเดตแจงนับข้างต้นได้ดังนี้:
enum Status: String, Codable {
case draft
case inReview = "in review"
case approved
case published
}
การปรับแต่งการทำแผนที่
บางครั้งชื่อแอตทริบิวต์ของเอกสาร Cloud Firestore ที่เราต้องการแมปไม่ตรงกับชื่อของคุณสมบัติในโมเดลข้อมูลของเราใน Swift ตัวอย่างเช่น เพื่อนร่วมงานคนหนึ่งของเราอาจเป็นนักพัฒนา Python และตัดสินใจเลือก Snake_case สำหรับชื่อแอตทริบิวต์ทั้งหมดของพวกเขา
ไม่ต้องกังวล: Codable ครอบคลุมถึงเราแล้ว!
สำหรับกรณีเช่นนี้ เราสามารถใช้ CodingKeys
ได้ นี่คือแจงนับที่เราสามารถเพิ่มลงในโครงสร้างที่เขียนโค้ดได้เพื่อระบุวิธีการแมปแอตทริบิวต์บางอย่าง
พิจารณาเอกสารนี้:
ในการแมปเอกสารนี้กับโครงสร้างที่มีคุณสมบัติชื่อประเภท 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
มันจะไม่ถูกแมปเมื่อดึงข้อมูล และกลายเป็น nil
ซึ่งจะส่งผลให้มุมมอง List
เต็มไปด้วยเอกสารแรก
คุณสมบัติใดๆ ที่ไม่อยู่ในรายการเป็นกรณีและปัญหาในการแจงนับ CodingKeys
ที่เกี่ยวข้องจะถูกละเว้นในระหว่างกระบวนการแม็ป สิ่งนี้อาจสะดวกจริง ๆ หากเราต้องการแยกคุณสมบัติบางอย่างออกจากการแมปโดยเฉพาะ
ตัวอย่างเช่น หากเราต้องการแยกคุณสมบัติ reasonWhyILoveThis
ออกจากการแมป สิ่งที่เราต้องทำคือลบมันออกจาก CodingKeys
enum:
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 hex) — สีเหล่านี้ใช้งานง่าย (โดยพื้นฐานแล้วเป็นเพียงสตริง) และยังรองรับความโปร่งใสอีกด้วย!
เพื่อให้สามารถแมป 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 จะโทรหาผู้ฟังของเราเมื่อใดก็ตามที่มี เป็นการอัพเดต
ต่อไปนี้คือข้อมูลโค้ดที่แสดงวิธีการลงทะเบียน 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
- หากหนังสือเป็นสิ่งที่คุณชอบมากกว่า ลองดูที่ Mattt's Flight School Guide to Swift Codable
- และสุดท้าย Donny Wals ก็มี ซีรีส์เกี่ยวกับ Codable ทั้งหมด
แม้ว่าเราจะพยายามอย่างดีที่สุดเพื่อรวบรวมคำแนะนำที่ครอบคลุมสำหรับการแมปเอกสาร Cloud Firestore แต่ก็ไม่ได้ครอบคลุมทั้งหมด และคุณอาจใช้กลยุทธ์อื่นในการแมปประเภทของคุณ ใช้ปุ่ม ส่งคำติชม ด้านล่าง เพื่อแจ้งให้เราทราบว่าคุณใช้กลยุทธ์ใดในการแมปข้อมูล Cloud Firestore ประเภทอื่นๆ หรือแสดงข้อมูลใน Swift
ไม่มีเหตุผลจริงๆ ที่จะไม่ใช้การสนับสนุน Codable ของ Cloud Firestore