ทุกวันนี้การพัฒนาแอปพลิเคชันมือถือสมัยใหม่จำเป็นต้องมีการวางแผนอย่างรอบคอบเพื่อให้ข้อมูลผู้ใช้ซิงค์กันในอุปกรณ์ต่างๆ นี่เป็นปัญหาที่หนักหน่วงกับ gotchas และข้อผิดพลาดมากมาย แต่ผู้ใช้คาดหวังว่าคุณลักษณะนี้และคาดว่าจะทำงานได้ดี
สำหรับ iOS และ macOS Apple มีชุดเครื่องมือที่แข็งแกร่งเรียกว่า CloudKit API ซึ่งช่วยให้นักพัฒนาที่กำหนดเป้าหมายไปที่แพลตฟอร์ม Apple เพื่อแก้ปัญหาการซิงโครไนซ์นี้
ในบทความนี้ฉันจะสาธิตวิธีใช้ CloudKit เพื่อให้ข้อมูลของผู้ใช้ซิงค์ระหว่างไคลเอนต์หลาย ๆ ตัว มีไว้สำหรับ นักพัฒนา iOS ที่มีประสบการณ์ ซึ่งคุ้นเคยกับกรอบงานของ Apple และ Swift ฉันจะเจาะลึกด้านเทคนิคเกี่ยวกับ CloudKit API เพื่อสำรวจวิธีที่คุณสามารถใช้ประโยชน์จากเทคโนโลยีนี้เพื่อสร้างแอปหลายอุปกรณ์ที่ยอดเยี่ยม ฉันจะเน้นไปที่แอปพลิเคชัน iOS แต่วิธีเดียวกันนี้ก็สามารถใช้กับไคลเอนต์ macOS ได้เช่นกัน
ตัวอย่างการใช้งานของเราเป็นแอปพลิเคชันโน้ตธรรมดาที่มีโน้ตเพียงตัวเดียวเพื่อจุดประสงค์ในการอธิบาย ระหว่างทางฉันจะดูแง่มุมที่ยุ่งยากกว่าของการซิงโครไนซ์ข้อมูลบนคลาวด์รวมถึงการจัดการข้อขัดแย้งและลักษณะการทำงานของเลเยอร์เครือข่ายที่ไม่สอดคล้องกัน
CloudKit สร้างขึ้นจากบริการ iCloud ของ Apple เป็นเรื่องยุติธรรมที่จะกล่าวว่า iCloud เริ่มต้นได้ยาก การเปลี่ยนแปลงที่เงอะงะจาก MobileMe ประสิทธิภาพที่ไม่ดีและแม้กระทั่งปัญหาความเป็นส่วนตัวบางอย่างก็ทำให้ระบบกลับมาในช่วงต้นปีที่ผ่านมา
สำหรับนักพัฒนาแอปสถานการณ์ยิ่งแย่ลงไปอีก ก่อนหน้า CloudKit พฤติกรรมที่ไม่สอดคล้องกันและเครื่องมือดีบักที่อ่อนแอทำให้แทบเป็นไปไม่ได้ที่จะส่งมอบผลิตภัณฑ์คุณภาพสูงโดยใช้ iCloud API รุ่นแรก
อย่างไรก็ตามเมื่อเวลาผ่านไป Apple ได้แก้ไขปัญหาเหล่านี้แล้ว โดยเฉพาะอย่างยิ่งหลังจากการเปิดตัว CloudKit SDK ในปี 2014 นักพัฒนาบุคคลที่สามมีโซลูชันทางเทคนิคที่มีคุณสมบัติครบถ้วนและมีประสิทธิภาพในการแชร์ข้อมูลบนคลาวด์ระหว่างอุปกรณ์ต่างๆ (รวมถึงแอปพลิเคชัน macOS และแม้แต่ไคลเอนต์บนเว็บ)
เนื่องจาก CloudKit มีความเชื่อมโยงอย่างลึกซึ้งกับระบบปฏิบัติการและอุปกรณ์ของ Apple จึงไม่เหมาะสำหรับแอปพลิเคชันที่ต้องการการสนับสนุนอุปกรณ์ที่กว้างขึ้นเช่นไคลเอ็นต์ Android หรือ Windows อย่างไรก็ตามสำหรับแอปที่กำหนดเป้าหมายไปยังฐานผู้ใช้ของ Apple จะมีกลไกที่ทรงพลังอย่างยิ่งสำหรับการตรวจสอบผู้ใช้และการซิงโครไนซ์ข้อมูล
CloudKit จัดระเบียบข้อมูลผ่านลำดับชั้นของคลาส: CKContainer
, CKDatabase
, CKRecordZone
และ CKRecord
ที่ระดับบนสุดคือ CKContainer
ซึ่งห่อหุ้มชุดข้อมูล CloudKit ที่เกี่ยวข้อง ทุกแอปจะได้รับค่าเริ่มต้น CKContainer
โดยอัตโนมัติและกลุ่มแอปสามารถแชร์ CKContainer
ที่กำหนดเองได้ หากการตั้งค่าอนุญาตอนุญาต ซึ่งสามารถเปิดใช้งานเวิร์กโฟลว์ข้ามแอปพลิเคชันที่น่าสนใจได้
ภายในแต่ละ CKContainer
เป็นหลายอินสแตนซ์ของ CKDatabase
CloudKit จะกำหนดค่าแอปที่เปิดใช้งาน CloudKit ทั้งหมดโดยอัตโนมัตินอกกรอบเพื่อให้มี CKDatabase
สาธารณะ (ผู้ใช้แอปทั้งหมดสามารถเห็นทุกอย่าง) และส่วนตัว CKDatabase
(ผู้ใช้แต่ละคนเห็นเฉพาะข้อมูลของตนเอง) และสำหรับ iOS 10 แชร์ CKDatabase
โดยที่กลุ่มที่ผู้ใช้ควบคุมสามารถแบ่งปันรายการระหว่างสมาชิกของกลุ่ม
ภายใน CKDatabase
คือ CKRecordZone
s และภายในโซน CKRecord
s คุณสามารถอ่านและเขียนบันทึกค้นหาระเบียนที่ตรงกับชุดของเกณฑ์และ (ที่สำคัญที่สุด) ได้รับการแจ้งเตือนการเปลี่ยนแปลงใด ๆ ข้างต้น
สำหรับแอป Note ของคุณคุณสามารถใช้คอนเทนเนอร์เริ่มต้นได้ ภายในคอนเทนเนอร์นี้คุณจะใช้ฐานข้อมูลส่วนตัว (เนื่องจากคุณต้องการให้ผู้ใช้เห็นบันทึกย่อของผู้ใช้เท่านั้น) และภายในฐานข้อมูลส่วนตัวคุณจะใช้โซนบันทึกที่กำหนดเองซึ่งเปิดใช้งานการแจ้งเตือนเฉพาะ บันทึกการเปลี่ยนแปลง
โน้ตจะถูกจัดเก็บเป็นไฟล์เดียว CKRecord
กับ text
, modified
(DateTime) และ version
ฟิลด์ CloudKit ติดตามภายใน modified
โดยอัตโนมัติ ค่า แต่คุณต้องการทราบเวลาที่แก้ไขจริงรวมถึงกรณีออฟไลน์เพื่อวัตถุประสงค์ในการแก้ไขข้อขัดแย้ง version
ฟิลด์เป็นเพียงภาพประกอบของแนวทางปฏิบัติที่ดีในการอัพเกรดการพิสูจน์อักษรโปรดทราบว่าผู้ใช้ที่มีอุปกรณ์หลายเครื่องอาจไม่อัปเดตแอปของคุณในทุกเครื่องพร้อมกันดังนั้นจึงมีบางคนเรียกร้องให้มีการป้องกัน
ฉันสมมติว่าคุณมีพื้นฐานที่ดีในการสร้างแอป iOS ใน Xcode หากต้องการคุณสามารถทำได้ ดาวน์โหลด และตรวจสอบตัวอย่างโครงการ Note App Xcode ที่สร้างขึ้นสำหรับบทช่วยสอนนี้
สำหรับวัตถุประสงค์ของเราแอปพลิเคชันมุมมองเดียวที่มี UITextView
ด้วย ViewController
ตามที่ผู้ได้รับมอบหมายจะเพียงพอ ในระดับแนวคิดคุณต้องการทริกเกอร์การอัปเดตระเบียน CloudKit เมื่อใดก็ตามที่ข้อความเปลี่ยนแปลง อย่างไรก็ตามในทางปฏิบัติควรใช้กลไกการรวมการเปลี่ยนแปลงบางอย่างเช่นตัวจับเวลาพื้นหลังที่เริ่มทำงานเป็นระยะเพื่อหลีกเลี่ยงการส่งสแปมเซิร์ฟเวอร์ iCloud ที่มีการเปลี่ยนแปลงเล็กน้อยมากเกินไป
แอป CloudKit ต้องการเปิดใช้งานบางรายการในบานหน้าต่างความสามารถของเป้าหมาย Xcode: iCloud (ตามธรรมชาติ) รวมถึงช่องทำเครื่องหมาย CloudKit, การแจ้งเตือนแบบพุชและโหมดพื้นหลัง (โดยเฉพาะการแจ้งเตือนระยะไกล)
สำหรับฟังก์ชัน CloudKit ฉันได้แบ่งสิ่งต่างๆออกเป็นสองคลาส: ระดับที่ต่ำกว่า CloudKitNoteDatabase
ซิงเกิลตันและระดับที่สูงขึ้น CloudKitNote
ชั้นเรียน.
แต่ก่อนอื่นให้พูดคุยเกี่ยวกับข้อผิดพลาด CloudKit อย่างรวดเร็ว
การจัดการข้อผิดพลาดอย่างรอบคอบเป็นสิ่งสำคัญอย่างยิ่งสำหรับไคลเอนต์ CloudKit
เนื่องจากเป็น API ที่ทำงานบนเครือข่ายจึงมีความอ่อนไหวต่อปัญหาด้านประสิทธิภาพและความพร้อมใช้งานทั้งหมด นอกจากนี้บริการยังต้องป้องกันปัญหาต่างๆที่อาจเกิดขึ้นเช่นคำขอที่ไม่ได้รับอนุญาตการเปลี่ยนแปลงที่ขัดแย้งกันและอื่น ๆ
CloudKit ให้ รหัสข้อผิดพลาดเต็มรูปแบบ พร้อมข้อมูลประกอบเพื่อให้นักพัฒนาจัดการกับกรณีขอบต่างๆและหากจำเป็นให้คำอธิบายโดยละเอียดแก่ผู้ใช้เกี่ยวกับปัญหาที่อาจเกิดขึ้น
นอกจากนี้การดำเนินการของ CloudKit หลายรายการสามารถส่งคืนข้อผิดพลาดเป็นค่าความผิดพลาดเดียวหรือข้อผิดพลาดแบบผสมที่ระบุที่ระดับบนสุดเป็น partialFailure
มันมาพร้อมกับพจนานุกรมของ CKError
s ที่สมควรได้รับการตรวจสอบอย่างรอบคอบมากขึ้นเพื่อค้นหาว่าเกิดอะไรขึ้นระหว่างการทำงานแบบผสม
เพื่อช่วยนำทางความซับซ้อนนี้คุณสามารถขยาย CKError
ด้วยวิธีการช่วยเหลือเล็กน้อย
โปรดทราบว่ารหัสทั้งหมดมีความคิดเห็นอธิบายที่ประเด็นสำคัญ
import CloudKit extension CKError { public func isRecordNotFound() -> Bool return isZoneNotFound() public func isZoneNotFound() -> Bool { return isSpecificErrorCode(code: .zoneNotFound) } public func isUnknownItem() -> Bool { return isSpecificErrorCode(code: .unknownItem) } public func isConflict() -> Bool { return isSpecificErrorCode(code: .serverRecordChanged) } public func isSpecificErrorCode(code: CKError.Code) -> Bool { var match = false if self.code == code { match = true } else if self.code == .partialFailure { // This is a multiple-issue error. Check the underlying array // of errors to see if it contains a match for the error in question. guard let errors = partialErrorsByItemID else { return false } for (_, error) in errors { if let cke = error as? CKError { if cke.code == code { match = true break } } } } return match } // ServerRecordChanged errors contain the CKRecord information // for the change that failed, allowing the client to decide // upon the best course of action in performing a merge. public func getMergeRecords() -> (CKRecord?, CKRecord?) { if code == .serverRecordChanged { // This is the direct case of a simple serverRecordChanged Error. return (clientRecord, serverRecord) } guard code == .partialFailure else { return (nil, nil) } guard let errors = partialErrorsByItemID else { return (nil, nil) } for (_, error) in errors { if let cke = error as? CKError { if cke.code == .serverRecordChanged { // This is the case of a serverRecordChanged Error // contained within a multi-error PartialFailure Error. return cke.getMergeRecords() } } } return (nil, nil) } }
CloudKitNoteDatabase
ซิงเกิลตันApple มีฟังก์ชันการทำงานสองระดับใน CloudKit SDK: ฟังก์ชัน 'ความสะดวก' ระดับสูงเช่น fetch()
, save()
และ delete()
และโครงสร้างการทำงานระดับล่างที่มีชื่อที่ยุ่งยากเช่น CKModifyRecordsOperation
.
API ความสะดวกสามารถเข้าถึงได้มากขึ้นในขณะที่แนวทางการดำเนินการอาจเป็นเรื่องที่น่ากลัวเล็กน้อย อย่างไรก็ตาม Apple ขอเรียกร้องให้นักพัฒนาใช้การดำเนินการมากกว่าวิธีการอำนวยความสะดวก
เทคโนโลยีแอปพลิเคชั่นประวัติศาสตร์เสมือนจริงและอนาคต
การทำงานของ CloudKit ให้การควบคุมที่เหนือกว่าเกี่ยวกับรายละเอียดว่า CloudKit ทำงานอย่างไรและที่สำคัญกว่านั้นคือบังคับให้นักพัฒนาคิดอย่างรอบคอบเกี่ยวกับพฤติกรรมเครือข่ายที่เป็นศูนย์กลางของทุกสิ่งที่ CloudKit ทำ ด้วยเหตุผลเหล่านี้ฉันใช้การดำเนินการในตัวอย่างโค้ดเหล่านี้
คลาสเดี่ยวของคุณจะรับผิดชอบการดำเนินการ CloudKit แต่ละอย่างที่คุณจะใช้ ในความเป็นจริงคุณกำลังสร้าง API เพื่อความสะดวกขึ้นมาใหม่ แต่ด้วยการปรับใช้ด้วยตัวคุณเองตาม Operation API คุณจะสามารถปรับแต่งพฤติกรรมและปรับแต่งการตอบสนองการจัดการข้อผิดพลาดได้ ตัวอย่างเช่นหากคุณต้องการขยายแอปนี้เพื่อรองรับ Notes หลาย ๆ อันแทนที่จะใช้เพียงแอปเดียวคุณสามารถทำได้อย่างง่ายดาย (และมีประสิทธิภาพที่สูงขึ้น) มากกว่าการใช้ API เพื่อความสะดวกของ Apple
import CloudKit public protocol CloudKitNoteDatabaseDelegate { func cloudKitNoteRecordChanged(record: CKRecord) } public class CloudKitNoteDatabase { static let shared = CloudKitNoteDatabase() private init() { let zone = CKRecordZone(zoneName: 'note-zone') zoneID = zone.zoneID } public var delegate: CloudKitNoteDatabaseDelegate? public var zoneID: CKRecordZoneID? // ... }
CloudKit จะสร้างโซนเริ่มต้นสำหรับฐานข้อมูลส่วนตัวโดยอัตโนมัติ อย่างไรก็ตามคุณจะได้รับฟังก์ชันเพิ่มเติมหากคุณใช้โซนที่กำหนดเองโดยเฉพาะอย่างยิ่งการสนับสนุนการดึงข้อมูลการเปลี่ยนแปลงระเบียนส่วนเพิ่ม
เนื่องจากนี่เป็นตัวอย่างแรกของการใช้การดำเนินการต่อไปนี้เป็นข้อสังเกตทั่วไปสองประการ:
ประการแรกการดำเนินการของ CloudKit ทั้งหมดมีการปิดแบบกำหนดเอง (และส่วนใหญ่มีการปิดระดับกลางขึ้นอยู่กับการดำเนินการ) CloudKit มีของตัวเอง CKError
คลาสมาจาก Error
แต่คุณต้องตระหนักถึงความเป็นไปได้ที่จะเกิดข้อผิดพลาดอื่น ๆ ด้วยเช่นกัน ประการสุดท้ายสิ่งที่สำคัญที่สุดประการหนึ่งของการดำเนินการใด ๆ คือ qualityOfService
มูลค่า. เนื่องจากเวลาในการตอบสนองของเครือข่ายโหมดบนเครื่องบินและเช่นนั้น CloudKit จะจัดการการลองซ้ำและการดำเนินการดังกล่าวเป็นการภายในที่ a qualityOfService
ของ 'ยูทิลิตี้' หรือต่ำกว่า ขึ้นอยู่กับบริบทคุณอาจต้องการกำหนด qualityOfService
ที่สูงขึ้น และจัดการกับสถานการณ์เหล่านี้ด้วยตัวคุณเอง
เมื่อตั้งค่าแล้วการดำเนินการจะถูกส่งไปยัง CKDatabase
ออบเจ็กต์ซึ่งจะถูกเรียกใช้บนเธรดพื้นหลัง
// Create a custom zone to contain our note records. We only have to do this once. private func createZone(completion: @escaping (Error?) -> Void) { let recordZone = CKRecordZone(zoneID: self.zoneID!) let operation = CKModifyRecordZonesOperation(recordZonesToSave: [recordZone], recordZoneIDsToDelete: []) operation.modifyRecordZonesCompletionBlock = { _, _, error in guard error == nil else { completion(error) return } completion(nil) } operation.qualityOfService = .utility let container = CKContainer.default() let db = container.privateCloudDatabase db.add(operation) }
การสมัครเป็นหนึ่งในคุณสมบัติของ CloudKit ที่มีค่าที่สุด พวกเขาสร้างโครงสร้างพื้นฐานการแจ้งเตือนของ Apple เพื่อให้ไคลเอนต์ต่างๆรับการแจ้งเตือนแบบพุชเมื่อมีการเปลี่ยนแปลง CloudKit บางอย่างเกิดขึ้น สิ่งเหล่านี้อาจเป็นการแจ้งเตือนแบบพุชตามปกติที่ผู้ใช้ iOS คุ้นเคย (เช่นเสียงแบนเนอร์หรือป้าย) หรือใน CloudKit อาจเป็นการแจ้งเตือนระดับพิเศษที่เรียกว่า ดันเงียบ . การผลักดันแบบเงียบเหล่านี้เกิดขึ้นโดยสิ้นเชิงโดยไม่มีการมองเห็นหรือการโต้ตอบของผู้ใช้ดังนั้นจึงไม่จำเป็นต้องให้ผู้ใช้เปิดใช้งานการแจ้งเตือนแบบพุชสำหรับแอปของคุณซึ่งช่วยให้คุณไม่ต้องปวดหัวกับประสบการณ์ของผู้ใช้ในฐานะนักพัฒนาแอป
วิธีเปิดใช้งานการแจ้งเตือนแบบไม่มีเสียงเหล่านี้คือการตั้งค่า shouldSendContentAvailable
คุณสมบัติบน CKNotificationInfo
ในขณะที่ออกจากการตั้งค่าการแจ้งเตือนแบบเดิมทั้งหมด (shouldBadge
, soundName
และอื่น ๆ ) โดยไม่ได้ตั้งค่า
โปรดทราบว่าฉันกำลังใช้ CKQuerySubscription
ด้วยเพรดิเคต 'จริงเสมอ' ที่เรียบง่ายมากเพื่อดูการเปลี่ยนแปลงในบันทึกหมายเหตุหนึ่ง (และเท่านั้น) ในแอปพลิเคชันที่ซับซ้อนยิ่งขึ้นคุณอาจต้องการใช้ประโยชน์จากเพรดิเคตเพื่อ จำกัด ขอบเขตของ CKQuerySubscription
โดยเฉพาะให้แคบลงและคุณอาจต้องการตรวจสอบการสมัครสมาชิกประเภทอื่น ๆ ที่มีอยู่ใน CloudKit เช่น CKDatabaseSuscription
สุดท้ายสังเกตว่าคุณสามารถใช้ UserDefaults
ค่าแคชเพื่อหลีกเลี่ยงการบันทึกการสมัครสมาชิกมากกว่าหนึ่งครั้งโดยไม่จำเป็น ไม่มีอันตรายใด ๆ ในการตั้งค่า แต่ Apple ขอแนะนำให้พยายามหลีกเลี่ยงปัญหานี้เนื่องจากเป็นการสิ้นเปลืองทรัพยากรเครือข่ายและเซิร์ฟเวอร์
// Create the CloudKit subscription we’ll use to receive notification of changes. // The SubscriptionID lets us identify when an incoming notification is associated // with the query we created. public let subscriptionID = 'cloudkit-note-changes' private let subscriptionSavedKey = 'ckSubscriptionSaved' public func saveSubscription() { // Use a local flag to avoid saving the subscription more than once. let alreadySaved = UserDefaults.standard.bool(forKey: subscriptionSavedKey) guard !alreadySaved else { return } // If you wanted to have a subscription fire only for particular // records you can specify a more interesting NSPredicate here. // For our purposes we’ll be notified of all changes. let predicate = NSPredicate(value: true) let subscription = CKQuerySubscription(recordType: 'note', predicate: predicate, subscriptionID: subscriptionID, options: [.firesOnRecordCreation, .firesOnRecordDeletion, .firesOnRecordUpdate]) // We set shouldSendContentAvailable to true to indicate we want CloudKit // to use silent pushes, which won’t bother the user (and which don’t require // user permission.) let notificationInfo = CKNotificationInfo() notificationInfo.shouldSendContentAvailable = true subscription.notificationInfo = notificationInfo let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: []) operation.modifySubscriptionsCompletionBlock = { (_, _, error) in guard error == nil else { return } UserDefaults.standard.set(true, forKey: self.subscriptionSavedKey) } operation.qualityOfService = .utility let container = CKContainer.default() let db = container.privateCloudDatabase db.add(operation) }
การเรียกบันทึกโดยใช้ชื่อนั้นตรงไปตรงมามาก คุณสามารถคิดว่าชื่อเป็นคีย์หลักของเรกคอร์ดในความหมายของฐานข้อมูลง่ายๆ (ชื่อต้องไม่ซ้ำกันเป็นต้น) จริง CKRecordID
มีความซับซ้อนกว่าเล็กน้อยซึ่งรวมถึง zoneID
CKFetchRecordsOperation
ดำเนินการกับบันทึกอย่างน้อยหนึ่งรายการในแต่ละครั้ง ในตัวอย่างนี้มีเพียงบันทึกเดียว แต่สำหรับความสามารถในการขยายในอนาคตนี่เป็นประโยชน์ด้านประสิทธิภาพที่ดี
// Fetch a record from the iCloud database public func loadRecord(name: String, completion: @escaping (CKRecord?, Error?) -> Void) { let recordID = CKRecordID(recordName: name, zoneID: self.zoneID!) let operation = CKFetchRecordsOperation(recordIDs: [recordID]) operation.fetchRecordsCompletionBlock = { records, error in guard error == nil else { completion(nil, error) return } guard let noteRecord = records?[recordID] else { // Didn't get the record we asked about? // This shouldn’t happen but we’ll be defensive. completion(nil, CKError.unknownItem as? Error) return } completion(noteRecord, nil) } operation.qualityOfService = .utility let container = CKContainer.default() let db = container.privateCloudDatabase db.add(operation) }
การบันทึกอาจเป็นการดำเนินการที่ซับซ้อนที่สุด วิธีง่ายๆในการเขียนบันทึกลงในฐานข้อมูลนั้นตรงไปตรงมาพอสมควร แต่ในตัวอย่างของฉันกับลูกค้าหลายรายนี่คือจุดที่คุณจะต้องเผชิญกับปัญหาที่อาจเกิดขึ้นจากการจัดการความขัดแย้งเมื่อลูกค้าหลายรายพยายามที่จะเขียนไปยังเซิร์ฟเวอร์พร้อมกัน โชคดีที่ CloudKit ได้รับการออกแบบมาอย่างชัดเจนเพื่อจัดการกับเงื่อนไขนี้ จะปฏิเสธคำขอเฉพาะที่มีบริบทข้อผิดพลาดเพียงพอในการตอบสนองเพื่อให้ไคลเอ็นต์แต่ละรายสามารถตัดสินใจได้อย่างรู้แจ้งในท้องถิ่นเกี่ยวกับวิธีแก้ไขข้อขัดแย้ง
แม้ว่าสิ่งนี้จะเพิ่มความซับซ้อนให้กับไคลเอ็นต์ แต่ท้ายที่สุดแล้วก็เป็นวิธีแก้ปัญหาที่ดีกว่าการให้ Apple มาพร้อมกับกลไกฝั่งเซิร์ฟเวอร์เพียงไม่กี่กลไกในการแก้ไขข้อขัดแย้ง
ตัวออกแบบแอปอยู่ในตำแหน่งที่ดีที่สุดในการกำหนดกฎสำหรับสถานการณ์เหล่านี้ซึ่งอาจรวมทุกอย่างตั้งแต่การผสานอัตโนมัติตามบริบทไปจนถึงคำแนะนำในการแก้ปัญหาที่ผู้ใช้กำหนดเอง ฉันจะไม่คิดอะไรมากในตัวอย่างของฉัน ฉันใช้ modified
เพื่อประกาศว่าการอัปเดตล่าสุดชนะ นี่อาจไม่ใช่ผลลัพธ์ที่ดีที่สุดสำหรับแอประดับมืออาชีพเสมอไป แต่ก็ไม่เลวสำหรับกฎข้อแรกและเพื่อจุดประสงค์นี้เพื่อแสดงให้เห็นกลไกที่ CloudKit ส่งข้อมูลความขัดแย้งกลับไปยังไคลเอนต์
โปรดทราบว่าในแอปพลิเคชันตัวอย่างของฉันขั้นตอนการแก้ไขข้อขัดแย้งนี้เกิดขึ้นใน CloudKitNote
ชั้นเรียนอธิบายในภายหลัง
// Save a record to the iCloud database public func saveRecord(record: CKRecord, completion: @escaping (Error?) -> Void) { let operation = CKModifyRecordsOperation(recordsToSave: [record], recordIDsToDelete: []) operation.modifyRecordsCompletionBlock = { _, _, error in guard error == nil else { guard let ckerror = error as? CKError else { completion(error) return } guard ckerror.isZoneNotFound() else { completion(error) return } // ZoneNotFound is the one error we can reasonably expect & handle here, since // the zone isn't created automatically for us until we've saved one record. // create the zone and, if successful, try again self.createZone() { error in guard error == nil else { completion(error) return } self.saveRecord(record: record, completion: completion) } return } // Lazy save the subscription upon first record write // (saveSubscription is internally defensive against trying to save it more than once) self.saveSubscription() completion(nil) } operation.qualityOfService = .utility let container = CKContainer.default() let db = container.privateCloudDatabase db.add(operation) }
การแจ้งเตือน CloudKit ให้วิธีการในการค้นหาว่าเมื่อใดที่มีการอัปเดตระเบียนโดยไคลเอนต์อื่น อย่างไรก็ตามเงื่อนไขของเครือข่ายและข้อ จำกัด ด้านประสิทธิภาพอาจทำให้การแจ้งเตือนแต่ละรายการหลุดออกไปหรือการแจ้งเตือนหลายรายการเพื่อรวมเข้าด้วยกันโดยเจตนาในการแจ้งเตือนไคลเอ็นต์เดียว เนื่องจากการแจ้งเตือนของ CloudKit สร้างขึ้นจากระบบการแจ้งเตือนของ iOS คุณจึงต้องระวังเงื่อนไขเหล่านี้
อย่างไรก็ตาม CloudKit มีเครื่องมือที่คุณต้องการสำหรับสิ่งนี้
แทนที่จะใช้การแจ้งเตือนแต่ละรายการเพื่อให้ความรู้โดยละเอียดเกี่ยวกับการเปลี่ยนแปลงของการแจ้งเตือนแต่ละรายการคุณใช้การแจ้งเตือนเพื่อระบุว่า บางอย่าง มีการเปลี่ยนแปลงจากนั้นคุณสามารถถาม CloudKit ว่ามีอะไรเปลี่ยนแปลงบ้างตั้งแต่ครั้งสุดท้ายที่คุณถาม ในตัวอย่างของฉันฉันทำได้โดยใช้ CKFetchRecordZoneChangesOperation
และ CKServerChangeTokens
. การเปลี่ยนโทเค็นสามารถคิดได้ว่าเป็นบุ๊กมาร์กที่บอกว่าคุณอยู่ที่ไหนก่อนที่จะมีการเปลี่ยนแปลงลำดับล่าสุดเกิดขึ้น
// Handle receipt of an incoming push notification that something has changed. private let serverChangeTokenKey = 'ckServerChangeToken' public func handleNotification() { // Use the ChangeToken to fetch only whatever changes have occurred since the last // time we asked, since intermediate push notifications might have been dropped. var changeToken: CKServerChangeToken? = nil let changeTokenData = UserDefaults.standard.data(forKey: serverChangeTokenKey) if changeTokenData != nil { changeToken = NSKeyedUnarchiver.unarchiveObject(with: changeTokenData!) as! CKServerChangeToken? } let options = CKFetchRecordZoneChangesOptions() options.previousServerChangeToken = changeToken let optionsMap = [zoneID!: options] let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [zoneID!], optionsByRecordZoneID: optionsMap) operation.fetchAllChanges = true operation.recordChangedBlock = { record in self.delegate?.cloudKitNoteRecordChanged(record: record) } operation.recordZoneChangeTokensUpdatedBlock = { zoneID, changeToken, data in guard let changeToken = changeToken else { return } let changeTokenData = NSKeyedArchiver.archivedData(withRootObject: changeToken) UserDefaults.standard.set(changeTokenData, forKey: self.serverChangeTokenKey) } operation.recordZoneFetchCompletionBlock = { zoneID, changeToken, data, more, error in guard error == nil else { return } guard let changeToken = changeToken else { return } let changeTokenData = NSKeyedArchiver.archivedData(withRootObject: changeToken) UserDefaults.standard.set(changeTokenData, forKey: self.serverChangeTokenKey) } operation.fetchRecordZoneChangesCompletionBlock = { error in guard error == nil else { return } } operation.qualityOfService = .utility let container = CKContainer.default() let db = container.privateCloudDatabase db.add(operation) }
ตอนนี้คุณมีหน่วยการสร้างระดับต่ำเพื่ออ่านและเขียนบันทึกและจัดการการแจ้งเตือนการเปลี่ยนแปลงบันทึก
ตอนนี้เรามาดูเลเยอร์ที่สร้างขึ้นด้านบนเพื่อจัดการการดำเนินการเหล่านี้ในบริบทของโน้ตเฉพาะ
CloudKitNote
คลาสสำหรับผู้เริ่มต้นสามารถกำหนดข้อผิดพลาดที่กำหนดเองบางอย่างเพื่อป้องกันไคลเอ็นต์จากภายในของ CloudKit และโปรโตคอลการมอบสิทธิ์แบบง่ายสามารถแจ้งไคลเอ็นต์ของการอัปเดตระยะไกลไปยังข้อมูล Note ที่อยู่ภายใต้
import CloudKit enum CloudKitNoteError : Error { case noteNotFound case newerVersionAvailable case unexpected } public protocol CloudKitNoteDelegate { func cloudKitNoteChanged(note: CloudKitNote) } public class CloudKitNote : CloudKitNoteDatabaseDelegate { public var delegate: CloudKitNoteDelegate? private(set) var text: String? private(set) var modified: Date? private let recordName = 'note' private let version = 1 private var noteRecord: CKRecord? public init() { CloudKitNoteDatabase.shared.delegate = self } // CloudKitNoteDatabaseDelegate call: public func cloudKitNoteRecordChanged(record: CKRecord) { // will be filled in below... } // … }
CKRecord
หมายเหตุใน Swift แต่ละฟิลด์บน a CKRecord
สามารถเข้าถึงได้ผ่านตัวดำเนินการตัวห้อย ค่าทั้งหมดเป็นไปตาม CKRecordValue
แต่ในทางกลับกันจะเป็นส่วนย่อยของชนิดข้อมูลที่คุ้นเคยเสมอ: NSString
, NSNumber
, NSDate
และอื่น ๆ .
นอกจากนี้ CloudKit ยังมีประเภทระเบียนเฉพาะสำหรับวัตถุไบนารี 'ขนาดใหญ่' ไม่มีการระบุจุดตัดที่เฉพาะเจาะจง (แนะนำให้ใช้รวมสูงสุด 1MB สำหรับแต่ละอัน CKRecord
) แต่ตามหลักทั่วไปแล้วสิ่งที่ให้ความรู้สึกเหมือนเป็นรายการอิสระ (ภาพเสียงหยดข้อความ ) แทนที่จะเป็นฟิลด์ฐานข้อมูลควรเก็บเป็น CKAsset
แนวทางปฏิบัตินี้ช่วยให้ CloudKit จัดการการถ่ายโอนเครือข่ายและที่เก็บข้อมูลฝั่งเซิร์ฟเวอร์ของรายการประเภทนี้ได้ดีขึ้น
สำหรับตัวอย่างนี้คุณจะใช้ CKAsset
เพื่อจัดเก็บข้อความบันทึก CKAsset
ข้อมูลจะถูกจัดการผ่านไฟล์ชั่วคราวในเครื่องที่มีข้อมูลที่เกี่ยวข้อง
// Map from CKRecord to our native data fields private func syncToRecord(record: CKRecord) -> (String?, Date?, Error?) { let version = record['version'] as? NSNumber guard version != nil else { return (nil, nil, CloudKitNoteError.unexpected) } guard version!.intValue <= self.version else { // Simple example of a version check, in case the user has // has updated the client on another device but not this one. // A possible response might be to prompt the user to see // if the update is available on this device as well. return (nil, nil, CloudKitNoteError.newerVersionAvailable) } let textAsset = record['text'] as? CKAsset guard textAsset != nil else { return (nil, nil, CloudKitNoteError.noteNotFound) } // CKAsset data is stored as a local temporary file. Read it // into a String here. let modified = record['modified'] as? Date do { let text = try String(contentsOf: textAsset!.fileURL) return (text, modified, nil) } catch { return (nil, nil, error) } }
การโหลดบันทึกนั้นง่ายมาก คุณทำการตรวจสอบข้อผิดพลาดที่จำเป็นเล็กน้อยจากนั้นดึงข้อมูลจริงจาก CKRecord
และจัดเก็บค่าในฟิลด์สมาชิกของคุณ
// Load a Note from iCloud public func load(completion: @escaping (String?, Date?, Error?) -> Void) { let noteDB = CloudKitNoteDatabase.shared noteDB.loadRecord(name: recordName) { (record, error) in guard error == nil else { guard let ckerror = error as? CKError else { completion(nil, nil, error) return } if ckerror.isRecordNotFound() { // This typically means we just haven’t saved it yet, // for example the first time the user runs the app. completion(nil, nil, CloudKitNoteError.noteNotFound) return } completion(nil, nil, error) return } guard let record = record else { completion(nil, nil, CloudKitNoteError.unexpected) return } let (text, modified, error) = self.syncToRecord(record: record) self.noteRecord = record self.text = text self.modified = modified completion(text, modified, error) } }
มีสถานการณ์พิเศษสองสามอย่างที่ต้องระวังเมื่อคุณบันทึกโน้ต
ก่อนอื่นคุณต้องแน่ใจว่าคุณเริ่มจาก CKRecord
ที่ถูกต้อง คุณถาม CloudKit ว่ามีบันทึกอยู่ที่นั่นหรือไม่และหากไม่มีให้สร้างใหม่ CKRecord
เพื่อใช้ในการบันทึกในภายหลัง
เมื่อคุณขอให้ CloudKit บันทึกระเบียนนี่คือที่ที่คุณอาจต้องจัดการกับข้อขัดแย้งเนื่องจากไคลเอนต์อื่นอัปเดตระเบียนตั้งแต่ครั้งสุดท้ายที่คุณดึงข้อมูล ในการคาดการณ์นี้ให้แบ่งฟังก์ชันบันทึกออกเป็นสองขั้นตอน ขั้นตอนแรกทำการตั้งค่าครั้งเดียวเพื่อเตรียมการเขียนบันทึกและขั้นตอนที่สองจะส่งผ่านบันทึกที่ประกอบลงไปที่ซิงเกิลตัน CloudKitNoteDatabase
ชั้นเรียน. ขั้นตอนที่สองนี้อาจทำซ้ำในกรณีที่มีความขัดแย้ง
ในกรณีที่มีข้อขัดแย้ง CloudKit จะมอบให้คุณในการส่งคืน CKError
สามเต็ม CKRecord
s เพื่อทำงานกับ:
โดยดูที่ modified
เขตข้อมูลของระเบียนเหล่านี้คุณสามารถตัดสินใจได้ว่าระเบียนใดเกิดขึ้นก่อนและจะเก็บข้อมูลใดไว้ หากจำเป็นคุณจะส่งบันทึกเซิร์ฟเวอร์ที่อัปเดตไปยัง CloudKit เพื่อเขียนบันทึกใหม่ แน่นอนว่าสิ่งนี้อาจส่งผลให้เกิดความขัดแย้งอีกครั้ง (หากมีการอัปเดตอื่นเข้ามาระหว่างนั้น) แต่คุณเพียงทำซ้ำขั้นตอนจนกว่าคุณจะได้ผลลัพธ์ที่สำเร็จ
ในแอปพลิเคชัน Note แบบธรรมดานี้เมื่อผู้ใช้คนเดียวสลับไปมาระหว่างอุปกรณ์คุณจะไม่เห็นความขัดแย้งมากเกินไปในแง่ของ 'การใช้งานพร้อมกันแบบสด' อย่างไรก็ตามความขัดแย้งดังกล่าวอาจเกิดขึ้นจากสถานการณ์อื่น ๆ ตัวอย่างเช่นผู้ใช้อาจทำการแก้ไขอุปกรณ์เครื่องหนึ่งขณะอยู่ในโหมดเครื่องบินจากนั้นจึงทำการแก้ไขที่แตกต่างกันในอุปกรณ์อื่นก่อนที่จะปิดโหมดเครื่องบินในอุปกรณ์เครื่องแรก
ในแอปพลิเคชันการแชร์ข้อมูลบนระบบคลาวด์สิ่งสำคัญอย่างยิ่งที่จะต้องมองหาทุกสถานการณ์ที่เป็นไปได้
// Save a Note to iCloud. If necessary, handle the case of a conflicting change. public func save(text: String, modified: Date, completion: @escaping (Error?) -> Void) { guard let record = self.noteRecord else { // We don’t already have a record. See if there’s one up on iCloud let noteDB = CloudKitNoteDatabase.shared noteDB.loadRecord(name: recordName) { record, error in if let error = error { guard let ckerror = error as? CKError else { completion(error) return } guard ckerror.isRecordNotFound() else { completion(error) return } // No record up on iCloud, so we’ll start with a // brand new record. let recordID = CKRecordID(recordName: self.recordName, zoneID: noteDB.zoneID!) self.noteRecord = CKRecord(recordType: 'note', recordID: recordID) self.noteRecord?['version'] = NSNumber(value:self.version) } else { guard record != nil else { completion(CloudKitNoteError.unexpected) return } self.noteRecord = record } // Repeat the save attempt now that we’ve either fetched // the record from iCloud or created a new one. self.save(text: text, modified: modified, completion: completion) } return } // Save the note text as a temp file to use as the CKAsset data. let tempDirectory = NSTemporaryDirectory() let tempFileName = NSUUID().uuidString let tempFileURL = NSURL.fileURL(withPathComponents: [tempDirectory, tempFileName]) do { try text.write(to: tempFileURL!, atomically: true, encoding: .utf8) } catch { completion(error) return } let textAsset = CKAsset(fileURL: tempFileURL!) record['text'] = textAsset record['modified'] = modified as NSDate saveRecord(record: record) { updated, error in defer { try? FileManager.default.removeItem(at: tempFileURL!) } guard error == nil else { completion(error) return } guard !updated else { // During the save we found another version on the server side and // the merging logic determined we should update our local data to match // what was in the iCloud database. let (text, modified, syncError) = self.syncToRecord(record: self.noteRecord!) guard syncError == nil else { completion(syncError) return } self.text = text self.modified = modified // Let the UI know the Note has been updated. self.delegate?.cloudKitNoteChanged(note: self) completion(nil) return } self.text = text self.modified = modified completion(nil) } } // This internal saveRecord method will repeatedly be called if needed in the case // of a merge. In those cases, we don’t have to repeat the CKRecord setup. private func saveRecord(record: CKRecord, completion: @escaping (Bool, Error?) -> Void) { let noteDB = CloudKitNoteDatabase.shared noteDB.saveRecord(record: record) { error in guard error == nil else { guard let ckerror = error as? CKError else { completion(false, error) return } let (clientRec, serverRec) = ckerror.getMergeRecords() guard let clientRecord = clientRec, let serverRecord = serverRec else { completion(false, error) return } // This is the merge case. Check the modified dates and choose // the most-recently modified one as the winner. This is just a very // basic example of conflict handling, more sophisticated data models // will likely require more nuance here. let clientModified = clientRecord['modified'] as? Date let serverModified = serverRecord['modified'] as? Date if (clientModified?.compare(serverModified!) == .orderedDescending) { // We’ve decided ours is the winner, so do the update again // using the current iCloud ServerRecord as the base CKRecord. serverRecord['text'] = clientRecord['text'] serverRecord['modified'] = clientModified! as NSDate self.saveRecord(record: serverRecord) { modified, error in self.noteRecord = serverRecord completion(true, error) } } else { // We’ve decided the iCloud version is the winner. // No need to overwrite it there but we’ll update our // local information to match to stay in sync. self.noteRecord = serverRecord completion(true, nil) } return } completion(false, nil) } }
เมื่อมีการแจ้งเตือนว่าบันทึกมีการเปลี่ยนแปลง CloudKitNoteDatabase
จะดำเนินการอย่างหนักในการดึงการเปลี่ยนแปลงจาก CloudKit ในกรณีตัวอย่างนี้จะเป็นเพียงบันทึกโน้ตเดียว แต่ก็ไม่ยากที่จะดูว่าจะขยายไปยังช่วงประเภทบันทึกและอินสแตนซ์ต่างๆได้อย่างไร
ตัวอย่างเช่นฉันได้รวมการตรวจสอบความมีสุขภาพจิตพื้นฐานเพื่อให้แน่ใจว่าฉันกำลังอัปเดตบันทึกที่ถูกต้องจากนั้นอัปเดตฟิลด์และแจ้งให้ผู้รับมอบสิทธิ์ทราบว่าเรามีข้อมูลใหม่
อำนาจของผู้ซื้อยกกระเป๋าห้ากองกำลัง
// CloudKitNoteDatabaseDelegate call: public func cloudKitNoteRecordChanged(record: CKRecord) { if record.recordID == self.noteRecord?.recordID { let (text, modified, error) = self.syncToRecord(record: record) guard error == nil else { return } self.noteRecord = record self.text = text self.modified = modified self.delegate?.cloudKitNoteChanged(note: self) } }
การแจ้งเตือน CloudKit มาถึงผ่านกลไกการแจ้งเตือนมาตรฐานของ iOS ดังนั้น AppDelegate
ของคุณ ควรโทร application.registerForRemoteNotifications
ใน didFinishLaunchingWithOptions
และใช้ didReceiveRemoteNotification
. เมื่อแอปได้รับการแจ้งเตือนให้ตรวจสอบว่าตรงกับการสมัครสมาชิกที่คุณสร้างขึ้นและหากเป็นเช่นนั้นให้ส่งต่อไปที่ CloudKitNoteDatabase
ซิงเกิลตัน
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { let dict = userInfo as! [String: NSObject] let notification = CKNotification(fromRemoteNotificationDictionary: dict) let db = CloudKitNoteDatabase.shared if notification.subscriptionID == db.subscriptionID { db.handleNotification() completionHandler(.newData) } else { completionHandler(.noData) } }
เคล็ดลับ: เนื่องจากการแจ้งเตือนแบบพุชไม่ได้รับการสนับสนุนอย่างสมบูรณ์ในโปรแกรมจำลอง iOS คุณจะต้องทำงานกับอุปกรณ์ iOS จริงในระหว่างการพัฒนาและทดสอบคุณสมบัติการแจ้งเตือนของ CloudKit คุณสามารถทดสอบการทำงานของ CloudKit อื่น ๆ ทั้งหมดในเครื่องจำลองได้ แต่คุณต้องลงชื่อเข้าใช้บัญชี iCloud ของคุณบนอุปกรณ์จำลอง
ไปเลย! ตอนนี้คุณสามารถเขียนอ่านและจัดการการแจ้งเตือนการอัปเดตจากระยะไกลสำหรับข้อมูลแอปพลิเคชันที่จัดเก็บ iCloud ของคุณโดยใช้ CloudKit API ที่สำคัญคุณมีพื้นฐานในการเพิ่มฟังก์ชัน CloudKit ขั้นสูง
นอกจากนี้ยังควรชี้ให้เห็นสิ่งที่คุณไม่ต้องกังวล: การตรวจสอบผู้ใช้ เนื่องจาก CloudKit ใช้ iCloud แอปพลิเคชันจึงอาศัยการตรวจสอบสิทธิ์ของผู้ใช้ทั้งหมดผ่านกระบวนการลงชื่อเข้าใช้ Apple ID / iCloud สิ่งนี้น่าจะช่วยประหยัดค่าใช้จ่ายในการพัฒนาและการดำเนินงานแบ็คเอนด์ได้อย่างมากสำหรับนักพัฒนาแอป
อาจเป็นเรื่องยากที่จะคิดว่าข้างต้นเป็นโซลูชันการแบ่งปันข้อมูลที่มีประสิทธิภาพอย่างสมบูรณ์ แต่ก็ไม่ง่ายอย่างนั้น
โดยนัยในทั้งหมดนี้ก็คือ CloudKit อาจไม่พร้อมใช้งานเสมอไป ผู้ใช้อาจไม่ได้ลงชื่อเข้าใช้พวกเขาอาจปิดใช้งาน CloudKit สำหรับแอปอาจอยู่ในโหมดเครื่องบินรายการข้อยกเว้นจะดำเนินต่อไป วิธีการบังคับที่ดุร้ายในการต้องการการเชื่อมต่อ CloudKit ที่ใช้งานอยู่เมื่อใช้แอปนั้นไม่เป็นที่พอใจเลยจากมุมมองของผู้ใช้และในความเป็นจริงอาจเป็นสาเหตุของการปฏิเสธจาก Apple App Store ดังนั้นโหมดออฟไลน์ต้องได้รับการพิจารณาอย่างรอบคอบ
ฉันจะไม่ลงรายละเอียดของการนำไปใช้ที่นี่ แต่โครงร่างควรเพียงพอ
ช่องหมายเหตุเดียวกันสำหรับข้อความและวันที่และเวลาที่แก้ไขสามารถเก็บไว้ในไฟล์ผ่าน NSKeyedArchiver
หรือสิ่งที่คล้ายกันและ UI สามารถให้ฟังก์ชันการทำงานใกล้เคียงเต็มตามสำเนาภายในเครื่องนี้ นอกจากนี้ยังสามารถทำให้เป็นอนุกรม CKRecords
โดยตรงไปยังและจากที่จัดเก็บในเครื่อง กรณีขั้นสูงเพิ่มเติมสามารถใช้ SQLite หรือเทียบเท่าเป็นฐานข้อมูลเงาสำหรับวัตถุประสงค์ในการทำซ้ำแบบออฟไลน์ จากนั้นแอปสามารถใช้ประโยชน์จากการแจ้งเตือนต่างๆที่มีให้โดย OS โดยเฉพาะ CKAccountChangedNotification
เพื่อให้ทราบว่าผู้ใช้ลงชื่อเข้าหรือออกเมื่อใดและเริ่มขั้นตอนการซิงโครไนซ์กับ CloudKit (แน่นอนว่ารวมถึงการแก้ไขข้อขัดแย้งที่เหมาะสม) ผลักดันการเปลี่ยนแปลงออฟไลน์ในเครื่องไปยังเซิร์ฟเวอร์และในทางกลับกัน
นอกจากนี้อาจเป็นที่พึงปรารถนาที่จะระบุ UI บางอย่างเกี่ยวกับความพร้อมใช้งานของ CloudKit สถานะการซิงค์และแน่นอนเงื่อนไขข้อผิดพลาดที่ไม่มีความละเอียดภายในที่น่าพอใจ
ในบทความนี้ฉันได้สำรวจกลไกหลักของ CloudKit API สำหรับการซิงค์ข้อมูลระหว่างไคลเอนต์ iOS หลายตัว
โปรดทราบว่ารหัสเดียวกันจะใช้ได้กับไคลเอนต์ macOS เช่นกันโดยมีการปรับเปลี่ยนเล็กน้อยสำหรับความแตกต่างในการทำงานของการแจ้งเตือนบนแพลตฟอร์มนั้น
CloudKit มอบฟังก์ชันการทำงานที่มากขึ้นโดยเฉพาะอย่างยิ่งสำหรับโมเดลข้อมูลที่ซับซ้อนการแชร์สาธารณะคุณสมบัติการแจ้งเตือนผู้ใช้ขั้นสูงและอื่น ๆ
แม้ว่า iCloud จะมีให้บริการสำหรับลูกค้า Apple เท่านั้น แต่ CloudKit มีแพลตฟอร์มที่ทรงพลังอย่างไม่น่าเชื่อในการสร้างแอพพลิเคชั่นหลายไคลเอนต์ที่น่าสนใจและใช้งานง่ายด้วยการลงทุนฝั่งเซิร์ฟเวอร์เพียงเล็กน้อย
หากต้องการเจาะลึกลงไปใน CloudKit เราขอแนะนำให้สละเวลาเพื่อดู การนำเสนอ CloudKit ที่หลากหลาย จาก WWDC สองสามรายการล่าสุดและทำตามตัวอย่างที่มีให้
ที่เกี่ยวข้อง: Swift Tutorial: บทนำเกี่ยวกับรูปแบบการออกแบบ MVVM