Codable API ของ Swift ซึ่งเปิดตั������ Swift 4 ����วยให้เราสามารถใช้พลังของ คอมไพเลอร์เพื่อแมปข้อมูลจากรูปแบบที่ต่อเนื่องกันไปยัง Swift ได้ง่ายขึ้น ประเภทต่างๆ
คุณอาจเคยใช้ Codable เพื่อจับคู่ข้อมูลจาก Web API กับข้อมูลของแอป (หรือกลับกัน) แต่โมเดลนี้มีความยืดหยุ่นกว่า
ในคู่มือนี้ เราจะดูวิธีใช้ Codable ในการแมปข้อมูลจาก Cloud Firestore เป็นประเภท Swift และในทางกลับกัน
เมื่อดึงข้อมูลเอกสารจาก Cloud Firestore แอปของคุณจะได้รับ พจนานุกรมของคู่คีย์/ค่า (หรืออาร์เรย์ของพจนานุกรม หากคุณใช้ การดำเนินการส่งกลับเอกสารหลายรายการ)
ตอนนี้คุณสามารถใช้พจนานุกรมใน Swift ได้โดยตรงและ ให้ความยืดหยุ่นที่ยอดเยี่ยมซึ่งเหมาะกับ Use Case ของคุณ อย่างไรก็ตาม วิธีการนี้ไม่ปลอดภัยและแนะนำได้ง่ายๆ ข้อบกพร่องที่ยากต่อการติดตามโดยการสะกดชื่อแอตทริบิวต์ผิดหรือลืมแมป แอตทริบิวต์ใหม่ที่ทีมของคุณเพิ่มไว้ เมื่อเปิดตัวฟีเจอร์ใหม่ที่น่าสนใจ เมื่อสัปดาห์ที่แล้ว
ในอดีต นักพัฒนาซอฟต์แวร์จำนวนมากได้แก้ไขข้อผิดพลาดเหล่านี้ การใช้เลเยอร์การแมปที่เรียบง่ายซึ่งช่วยให้พวกเขาสามารถจับคู่พจนานุกรม ประเภท 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 เป็นชื่อแทน สำหรับโปรโตคอลที่เข้ารหัสได้และถอดรหัสได้ การปฏิบัติตามประเภท Swift เป็นเช่นนี้ แล้วคอมไพเลอร์จะสังเคราะห์โค้ดที่จำเป็นต่อการเข้ารหัส/ถอดรหัส อินสแตนซ์ประเภทนี้จากรูปแบบที่ต่อเนื่องกัน เช่น JSON
รูปแบบง่ายๆ ในการจัดเก็บข้อมูลเกี่ยวกับหนังสืออาจมีลักษณะดังนี้
struct Book: Codable {
var title: String
var numberOfPages: Int
var author: String
}
จะเห็นได้ว่าการปรับประเภทให้เขียนโค้ดได้ถือเป็นการรุกล้ำพื้นที่น้อยที่สุด เราเท่านั้น จำเป็นต้องระบุความสอดคล้องดังกล่าวลงในโปรโตคอลด้วย ไม่ต้องทำการเปลี่ยนแปลงอื่นๆ
ตอนนี้เราสามารถเข้ารหัสหนังสือเป็นออบเจ็กต์ 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]
}
ข้อความสั้นๆ เกี่ยวกับการแมปรหัสเอกสาร
เราจะมาพูดถึงการแมป ID เอกสารกันก่อน ครู่หนึ่ง
เราใช้ Wrapper พร็อพเพอร์ตี้ @DocumentID
ในตัวอย่างก่อนหน้านี้บางส่วน
เพื่อแมปรหัสเอกสารของเอกสาร Cloud Firestore กับพร็อพเพอร์ตี้ id
ของประเภท Swift ซึ่งมีความสำคัญเนื่องจากเหตุผลหลายประการ:
- ซึ่งจะช่วยให้เราทราบว่าควรอัปเดตเอกสารใดในกรณีที่ผู้ใช้ระบุถึง การเปลี่ยนแปลง
List
ของ SwiftUI กำหนดให้องค์ประกอบเป็นIdentifiable
เพื่อ เพื่อป้องกันไม่ให้องค์ประกอบเคลื่อนไหวเมื่อแทรก
โปรดทราบว่าแอตทริบิวต์ที่ทำเครื่องหมายเป็น @DocumentID
จะไม่
เข้ารหัสโดยโปรแกรมเปลี่ยนไฟล์ของ Cloud Firestore เมื่อเขียนเอกสารกลับ นี่คือ
เนื่องจาก ID เอกสารไม่ใช่แอตทริบิวต์ของตัวเอกสารเอง ดังนั้น
การเขียนลงเอกสารนั้นถือเป็นความผิดพลาด
เมื่อทำงานกับประเภทที่ซ้อนกัน (เช่น อาร์เรย์ของแท็กใน 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)
หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับการค้นหาเอกสารตามสถานที่ตั้งจริง โปรดดูที่ คู่มือการใช้โซลูชันนี้
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 ที่เราสามารถ
เพิ่มลงในโครงสร้างโค้ดได้เพื่อระบุวิธีจับคู่แอตทริบิวต์บางรายการ
ลองพิจารณาเอกสารนี้:
ในการแมปเอกสารนี้กับ Struct ที่มีพร็อพเพอร์ตี้ชื่อประเภท 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 มีแนวคิดเกี่ยวกับตัวเลือกเพื่อแสดงถึง
ไม่มีค่า และ Cloud Firestore รองรับค่า null
เช่นกัน
อย่างไรก็ตาม ลักษณะการทำงานเริ่มต้นของตัวเลือกการเข้ารหัสที่มีค่า nil
คือ
ที่จะข้ามคำเหล่านั้นไป @ExplicitNull
ทำให้เราสามารถควบคุมวิธีการที่ Swift
ระบบจะจัดการกับตัวเลือกที่ไม่บังคับเมื่อเข้ารหัส โดยทำเครื่องหมายพร็อพเพอร์ตี้ที่ไม่บังคับว่า
@ExplicitNull
เราสามารถบอกให้ Cloud Firestore เขียนพร็อพเพอร์ตี้นี้ลงในส่วน
เอกสารที่มีค่า Null หากมีค่า nil
การใช้โปรแกรมเปลี่ยนไฟล์และตัวถอดรหัสที่กำหนดเองสำหรับการจับคู่สี
หัวข้อสุดท้ายเกี่ยวกับความครอบคลุมของข้อมูลแผนที่ด้วย Codable มาดู โปรแกรมเปลี่ยนไฟล์และตัวถอดรหัสที่กำหนดเอง ส่วนนี้ไม่ครอบคลุมโฆษณาเนทีฟ ประเภทข้อมูล Cloud Firestore แต่โปรแกรมเปลี่ยนไฟล์และตัวถอดรหัสที่กำหนดเองมีประโยชน์อย่างยิ่ง ในแอป Cloud Firestore
"ฉันจะจับคู่สีได้อย่างไร" เป็นคำถามหนึ่งที่นักพัฒนาซอฟต์แวร์ถามบ่อย ไม่เพียงสำหรับ Cloud Firestore แต่สำหรับการแมประหว่าง Swift และ JSON เป็น มีโซลูชันมากมายให้ใช้งาน แต่ส่วนใหญ่มุ่งเน้นที่ JSON และเกือบทุกสีจะจับคู่สีเป็นพจนานุกรมที่ซ้อนกันซึ่งประกอบขึ้นเป็น RGB คอมโพเนนต์
ดูเหมือนว่าจะมีวิธีแก้ปัญหาที่ดีกว่าและง่ายกว่านี้ ทำไมเราถึงไม่ใช้สีของเว็บ (หรือถ้าจะให้เฉพาะเจาะจงมากขึ้นก็คือรูปแ��บสีเลขฐาน 16 ของ CSS) รูปแบบเหล่านี้ใช้งานง่าย (โดยพื้นฐานแล้วคือสตริง) และยังสนับสนุนความโปร่งใสอีกด้วย!
เพื่อให้จับคู่ Color
ของ Swift กับค่าเลขฐานสิบหกได้ เราต้องสร้าง 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)"
}
}
}
}
}
การจัดการข้อผิดพลาดในการอัปเดตแบบเรียลไทม์
ข้อมูลโค้ดก่อนหน้านี้แสดงวิธีจัดการกับข้อผิดพลาดเมื่อดึงข้อมูล เอกสารเดียว นอกจากการดึงข้อมูล 1 ครั้งแล้ว 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 มีบทความดีๆ เกี่ยวกับ Basics of Codable
- หากสนใจเกี่ยวกับหนังสือมากกว่า ให้ไปที่ Flight School Guide to Swift Codable ของ Mattt
- และสุดท้าย Donny Wals มีซีรีส์เกี่ยวกับ Codable ทั้งซีรีส์
แม้ว่าเราจะพยายามอย่างสุดความสามารถในการรวบรวมคู่มือที่ครอบคลุมสำหรับการทำแผนที่ เอกสาร Cloud Firestore เป็นเพียงตัวอย่างบางส่วนเท่านั้น และคุณอาจกำลังใช้ กลยุทธ์อื่นๆ ในการจัดทำประเภทของคุณได้ ให้ใช้ปุ่มส่งความคิดเห็นด้านล่าง โปรดบอกให้เราทราบว่ากลยุทธ์ใดที่คุณใช้สำหรับการทำแผนที่ประเภทอื่นๆ ข้อมูล Cloud Firestore หรือแสดงข้อมูลใน Swift
ไม่มีเหตุผลที่จะไม่ใช้การรองร��บ Codable ของ Cloud Firestore