Swift +CloudKit開發入門篇
譯文簡介
CloudKit是蘋果公司推出的基于iCloud的遠程數據存儲服務。它為存儲和使用用戶的iCloud賬戶的后端存儲服務共享應用數據提供了一種低成本的選擇方案。
總體來看,CloudKit主要提供了兩個組件:
- 一個網絡中心,用于管理記錄類型和任何公共數據。
- 一組API,用于在iCloud和設備之間傳輸數據。
CloudKitr的安全性是很高的。用戶的私有數據受到完全保護,因為開發人員只能訪問他們自己的私人數據庫而不能查看任何其他用戶的私人數據。
對于只運行于iOS平臺的使用大量數據但不需要服務器端大量邏輯的應用程序來說,CloudKit是一個不錯的選擇。此外,CloudKit也可用于網絡和服務器應用程序。
在這個CloudKit教程中,您將獲得使用CloudKit的切身體驗,即你需要創建一個名為BabiFüd的餐廳評級應用程序。
為何選擇CloudKit?
你可能想知道,為什么要選擇基于核心數據(Core Data)基礎之上的CloudKit,而不是其他BaaS(后端即服務)產品,甚至是基于你自己的服務器?
原因有三:簡單性,高信譽度和低成本。
簡單性
不像其他的后端解決方案,CloudKit僅需要很少的設置。你不必選擇、配置或安裝服務器。此外,安全性和伸縮性也都由蘋果公司來處理。
只需在蘋果官方網站注冊成為iOS開發者項目成員,您就擁有了使用CloudKit的資格。你不必注冊額外的服務或創建新帳戶。當您在自己的應用程序中啟用CloudKit支持時,所有必要的服務器設置都將魔術般自動發生。
你不需要下載額外的庫并對它們進行配置。CloudKit就像任何其他iOS框架一樣導入。CloudKit框架本身通過提供一些針對常見操作的便利的API實現了一定程度的簡單性。
這也方便了用戶使用。由于在設備設置(甚至是設置結束進入應用程序時)時CloudKit使用的是輸入的iCloud憑據,所以沒有必要建立復雜的登錄屏幕。只要用戶登錄,他們就可以無縫地開始使用你的應用程序。
高信譽度
CloudKit的另一個好處是,通過依靠蘋果公司而不是應用程序開發人員,用戶可以相信他們的數據的隱私性和安全性。CloudKit能夠把您(開發人員)與用戶數據隔離開來。
雖然在調試程序時無法訪問可能令人沮喪,但另一個方面這也帶來一定好處,因為你不必擔心安全或說服用戶其數據的安全性。如果一個應用程序用戶信任iCloud,那么他們也可以信任作為開發人員的你。
低成本
***,對于任何開發者來說,運行服務的成本也是一個巨大的投資。即使是***的服務器主機也不能為小型、免費或廉價的應用程序提供低成本的解決方案。所以總是會有與運行應用程序相關的成本問題。
借助于CloudKit,你可以實現針對免費的公共數據的適量的存儲和數據傳輸。在WWDC 2015視頻的CloudKit新增功能(https://developer.apple.com/videos/play/wwdc2015/704/)中提供了非常詳盡的收費解釋。
上述所有這些優勢,使得CloudKit服務成為Mac和iOS應用程序開發中更值得選擇的解決方案。
BabiFüd項目功能介紹
本教程中提供的BabiFüd示例應用程序使用時下最標準的“速度餐廳”型應用程序風格。不是沿用傳統式的基于食品質量、服務速度以及價格等評價標準,而是使用新型的兒童友好性進行評價。這包括設施更換、加高座椅和健康性食品選擇等可用性標準。
應用程序包含四個選項卡:一個附近的餐館列表;一個附近餐館地圖展示;用戶生成注釋和功能設置。附近的餐館列表選項卡是您將在本教程中使用的唯一部分。當然,你現在就可以一瞥運行中的這個示例應用程序。
開發過程中,我使用了一個模型類來支持這些視圖,并封裝對CloudKit的調用。其中,CloudKit對象稱為記錄(Record)。模型中主要的記錄類型是一個Establishment,它代表了你的應用程序中不同的餐館。
開始
首先,請下載本教程中的啟動項目,地址是https://cdn2.raywenderlich.com/wp-content/uploads/2016/06/BabiFud-Cloudkit-Starter.zip。
你必須改變你的應用程序的資源標識符和團隊類型,然后才能開始編碼。為了從蘋果公司獲得必要的授權,您需要設置團隊。擁有一個獨特的資源標識符可以使得很多的事更容易操作。
現在,請使用Xcode打開工程BabiFud.xcodeproj。然后,從「Project Navigator」下選擇BabiFud項目,然后選擇「BabiFud target」。接下來,選擇「General」選項卡,使用一些獨特的字符串內容更換資源標識符(Bundle Identifier)。標準的做法是,使用反向域名符號并包括項目名稱。然后,選擇合適的團隊(Team):
現在,你需要在你的應用程序中設置CloudKit支持,并創建一些容器來保存數據。
權限和容器
在向應用程序中添加任何數據之前,你需要一個容器來保存應用程序的記錄。容器,其實僅是一個概念上的位置術語,對應于所有應用程序在服務器上的數據。它分為公共和私有數據庫兩個組。
要創建一個容器,你首先需要擁有啟用您的應用程序的iCloud權限。現在,請從目標編輯器選擇【Capabilities】選項卡。然后,把iCloud部分中的開關切換為ON。
現在,Xcode可能會提示您輸入與您的iOS開發者帳戶相關聯的蘋果ID。如果是這樣,那么根據要求輸入即可。***,通過勾選【Services】組中的CloudKit復選框來啟用CloudKit。
這將創建一個名為iCloud.<your app’s bundle id>的默認容器,如下圖所示:
克服Xcode中iCloud安裝錯誤
如果你在創建權限、構建項目或運行應用程序時看到任何警告或錯誤,并注意到Xcode抱怨容器ID;那么,下面提供一些故障排除提示,供您參考:
- 應用程序的包ID和iCloud的容器要相匹配并存在于開發人員帳戶中,這一點是很重要的。例如,如果包標識為“com.<your domain>.BabiFud”,那么,iCloud的容器名稱應是“iCloud.”,再加上資源包ID“iCloud.com.<your domain>.BabiFud”。
- iCloud的容器名稱必須是唯一的,因為這是使用CloudKit訪問數據的全球標識符。由于iCloud的容器名稱包含了資源ID,所以該資源ID也必須是唯一的(這就是為什么它必須由com.raywendrelich.BabiFud進行修改的原因)。
- 為了過權限這一關,應用程序/包ID顯示于【Certificates】,【Identifiers】和【Profiles】的App ID部分。這意味著,用于簽名應用程序的證書必須來自于設置的團隊ID并且必須列出應用程序ID;這也就是iCloud的容器id。通常情況下,如果你登錄了一個有效的開發帳戶,Xcode能夠自動地完成這一切。不幸的是,這有時會導致不同步情況。你可以通過一個新的ID刷新來重新開始,并使用iCloud的功能窗格更改CloudKit容器ID以便進行匹配。否則,要解決這個問題,你可能要編輯info.plist文件或BabiFud.entitlements文件,以確保ID值反映你為應用程序設置的包ID。
CloudKit控制面板簡介
在創建完工程所必需的餐館數據之后,下一步就是創建一些記錄類型來定義您的應用程序所使用的數據。您可以使用CloudKit控制面板來實現這一點。從工程的【Capabilities】窗格中點擊【CloudKit Dashboard】,如圖所示。
【注意】你還可以通過在瀏覽器中打開網址https://icloud.developer.apple.com/dashboard/ 來啟動CloudKit控制面板。
這個控制面板是什么樣子呢?請觀察一下這個圖形:
注意到,該控制面板由四部分組成:架構(Schema),公共數據(Public Data),私有數據(Private Data)和管理員(Admin)。
其中,Schema部分代表CloudKit容器中的***層次對象:記錄類型(Record Types),安全角色(Security Roles)及訂閱類型(Subscription Types)。在本教程中,你只要關心記錄類型即可。
記錄類型(Record Types),是一組定義了單個記錄的字段。對于面向對象編程而言,一個記錄類型就像一個類。一個記錄可以被認為是一個特殊的記錄類型的實例。它代表了容器中的結構化數據,就像數據庫中的一個典型的行,它封裝了一系列鍵/值對。
公共數據(PUBLIC DATA)和私有數據(PRIVATE DATA)部分,可以讓您在您能夠訪問的數據庫中添加數據或搜索數據。請記住,作為一名開發人員你可以訪問所有的公共數據,但只能訪問你自己的私有數據。其中,「User Records」存儲關于當前iCloud用戶的數據,如姓名和電子郵件等。「記錄區域」(Record Zone)(這里使用的是默認的區域),用于提供一個邏輯組織到一個私有數據庫;實現方法是對記錄進行分組。注意,自定義區域支持原子事務,即允許在處理其它操作之前把多個記錄同時保存。不過,有關自定義區域的探討已經超出本教程的范圍。
管理員(ADMIN)部分,能夠針對您的團隊成員提供不同的權限配置控制。如果你有多個開發團隊成員,你可以在這里限制他們編輯數據的能力。當然,這也超出本教程范圍。
添加餐館記錄類型
現在,請略微想一想你的應用程序的設計吧。你要跟蹤的每一家餐館都對應大量的數據:名稱、位置以及各種兒童友好的設施的可用性,等等。記錄類型使用字段來定義每個記錄包含的各個部分中的數據。
選擇記錄類型(Record Types)后,單擊詳細信息窗格左上方的+圖標添加一個新的記錄類型,如圖所示。
命名你剛剛創建的新記錄類型為「Establishment」。
你會看到出現一行字段,其中定義了字段名、字段類型和索引等,如下圖所示。注意,有一個字段使用了默認名稱「StringField」,這是系統自動為您創建的。
接下來,你要使用新名字「Name」替換「StringField」。字段類型和索引默認已匹配您所需要的***個字段的定義,但接下來,你需要對于其他一些字段改變字段類型和索引。單擊【Add Field…】并根據需要增加新的字段即可。***,你需要添加以下字段:
當你添加完所有字段后,你的字段列表應該是這樣的:
點擊頁面底部【Save】按鈕保存您的新的記錄類型。
現在,您已經準備好向您的數據庫中添加一些示例記錄了。
選擇左側導航窗格中【PUBLIC DATA】下部的【Default Zone】。該區域將包含您的應用程序的所有公共記錄。如果還沒有選定,那么請從中心窗格的下拉列表中選擇「Establishment」記錄類型。然后單擊右側詳細信息窗格中的+圖標或【New Record】按鈕,如下圖所示:
這將創建一個新的空的Establishment記錄。
此時,您已經準備好為您的應用程序輸入一些測試數據了。
需要說明的是,下面的示例數據都是虛構的。這些餐館數據都位于蘋果總部附近,這樣它們可以很容易地出現在模擬器上。
現在,請輸入如下表所述的每個記錄:
【注意】每個CoverPhoto元素對應的圖像文件都包含在Xcode項目的「Supporting Files\Sample Images」文件夾中。要將圖像添加到Establishment記錄中,只需將其拖動到CoverPhoto字段中即可。
一旦保存完所有三個記錄,控制面板應該是這樣的:
對于每個記錄,輸入的值都代表了數據的數據庫描述部分。在應用程序中,數據類型是不同的。例如,SeatingType和ChangingTable都是結構類型的。所以,對于SeatingType字段指定的int值可能對應于“high chair”或“booster”這樣的座位。對于HealthyOption和KidsMenu這兩個字段指定的int值表示布爾類型:其中,0是指沒有這一項,1則指有這一項。
***,運行應用程序需要你有一個可以用于開發的iCloud帳戶。請參考蘋果官方文檔https://developer.apple.com/library/tvos/documentation/DataManagement/Conceptual/CloudKitQuickStart/EnablingiCloudandConfiguringCloudKit/EnablingiCloudandConfiguringCloudKit.html#//apple_ref/doc/uid/TP40014987-CH2-SW7。
另外,您還需要在iPhone模擬器中輸入與此帳戶相關聯的icloud憑據。請參考蘋果官方文檔https://developer.apple.com/library/tvos/documentation/DataManagement/Conceptual/CloudKitQuickStart/CreatingaSchemabySavingRecords/CreatingaSchemabySavingRecords.html#//apple_ref/doc/uid/TP40014987-CH3-SW12。
現在,切換回Xcode中。從下一節開始,你要把上面創建的數據集成到您的應用程序中!
查詢餐館記錄
CKQuery對象用于從數據庫中選擇記錄。CKQuery描述了如何找到符合特定條件的特定類型的所有記錄。這些條件可以是這樣的:所有記錄都有一個以N開頭的Name字段;所有記錄都帶有兒童增高座椅;所有記錄都要滿足在3公里以內。這些類型的表達式都通過NSPredicate對象編碼于Cocoa庫中。NSPredicate能夠評估對象,看它們是否符合指定條件。NSPredicate也用在核心數據(Core Data)中,并且自然地融入CloudKit中,因為謂詞通常被定義用于針對某個字段的比較方面。
事實上,CloudKit僅支持NSPredicate功能的一個子集。這些功能包括:數學比較,字符串和集合操作(例如“字段匹配列表中的項目之一”),還提供了一個特殊的距離函數。函數distanceToLocation:fromLocation:被專門添加到NSPredicate對象定義中,特別支持CloudKit以匹配帶有位置字段的記錄——此已知位置位于指定半徑的范圍內。接下來的內容中將詳細介紹這種類型謂詞的用法。對于其他類型的查詢中,CKQuery類參考文檔(https://developer.apple.com/library/ios/documentation/CloudKit/Reference/CKQuery_class/)中包含了有關其支持的功能及如何使用的詳細描述。
【注意】CloudKit包括了對CLLocation對象的支持。有一些核心定位框架對象(Core Location Framework)包含了有關地理坐標的信息。這使得開發人員可以很容易創建一個查詢來尋找指定地理區域內的餐館——而不需要我們自己進行所有繁瑣的坐標運算。
接下來,在Xcode中打開文件Model/ Model.swift。該文件中包含了所有您的應用程序進行服務器調用的存根。
現在,請使用下面的內容來更換fetchEstablishments(_:radiusInMeters:)方法:
- func fetchEstablishments(location:CLLocation, radiusInMeters:CLLocationDistance) {
- // 1
- let radiusInKilometers = radiusInMeters / 1000.0
- // 2
- let locationPredicate = NSPredicate(format: "distanceToLocation:fromLocation:(%K,%@) < %f", "Location", location, radiusInKilometers)
- // 3
- let query = CKQuery(recordType: EstablishmentType, predicate: locationPredicate)
- // 4
- publicDB.performQuery(query, inZoneWithID: nil) { [unowned self] results, error in
- if let errorerror = error {
- dispatch_async(dispatch_get_main_queue()) {
- self.delegate?.errorUpdating(error)
- print("Cloud Query Error - Fetch Establishments: \(error)")
- }
- return
- }
- self.items.removeAll(keepCapacity: true)
- results?.forEach({ (record: CKRecord) in
- self.items.append(Establishment(record: record,
- database: self.publicDB))
- })
- dispatch_async(dispatch_get_main_queue()) {
- self.delegate?.modelUpdated()
- }
- }
- }
現在,我們按編號來分析一下上面代碼的功能:
1. CloudKit在其距離謂詞中利用公里為計算單位。這一行簡單地把radiusInMeters轉換為公里。
2. 根據從當前位置到他們的位置的距離謂詞對餐館進行篩選。這個語句使用從用戶的當前位置到指定距離內的位置值來查找所有餐館。
3. 使用謂詞和記錄類型創建CKQuery對象。執行查詢時兩者都將被使用。
4. ***,方法performQuery(_:inZoneWithID:completionHandler :)負責發送查詢到iCloud中,并等待任何匹配的結果。通過傳遞nil作為inZoneWithID參數值,可以針對你所在的默認區域進行查詢;也就是說,針對你的公共數據庫。如果你既想從公共數據庫也想從私有數據庫中檢索記錄,那么,你必須使用一個單獨的調用來查詢每個數據庫。
你可能想知道:CKDatabase的實例publicDB從何而來?讓我們來看看文件Model.swift的頂部的代碼吧。
- let container: CKContainer
- let publicDB: CKDatabase
- let privateDB: CKDatabase
- init() {
- // 1
- container = CKContainer.defaultContainer()
- // 2
- publicDB = container.publicCloudDatabase
- // 3
- privateDB = container.privateCloudDatabase
- }
在這里,您定義了您的數據庫:
1. 默認容器是指您在iCloud的功能窗格中指定的那個。
2. 公共數據庫是指在你的應用程序的所有用戶中共享的那個。
3. 私有數據庫只包含屬于當前登錄的用戶(在本實例中即指你)的數據。
總之,此代碼將從公共數據庫中檢索一些地方餐館,但為了在任何應用程序中看到相應的數據,必須把它關聯到一個視圖控制器。
創建需求回調函數
你可以借助熟悉的委托模式來管理通知的問題。下面這個協議位于Model.swift文件的頂部;當然,你需要在你的視圖控制器中實現這個協議:
- protocol ModelDelegate {
- func errorUpdating(error: NSError)
- func modelUpdated()
- }
- 現在,打開文件MasterViewController.swift,并用以下內容替換modelUpdated()方法原有內容:
- func modelUpdated() {
- refreshControl?.endRefreshing()
- tableView.reloadData()
- }
當新數據可用時此方法會被調用。在方法tableView(_:cellForRowAtIndexPath:)中實現了所有關于表格視圖單元格與CloudKit對象綁定的代碼。您可以自行分析學習,恕在此不再贅述。
接下來,在文件MasterViewController.swift中,請使用如下內容更換errorUpdating(_ :)方法:
- func errorUpdating(error: NSError) {
- let alertController = UIAlertController(title: nil,
- message: error.localizedDescription,
- preferredStyle: .Alert)
- alertController.addAction(UIAlertAction(title: "Dismiss",
- style: .Default,
- handler: nil))
- presentViewController(alertController, animated: true, completion: nil)
- }
當查詢產生錯誤時調用此方法。可能由于網絡條件較差或特定CloudKit問題(如丟失或不正確的用戶憑據或沒有記錄從查詢返回等)發生錯誤。
當處理任何一種遠程服務器連接時,良好的錯誤處理是必不可少的。現在,這段代碼只顯示給用戶返回的錯誤信息。
但是,目前程序中存在的非常普遍的問題是:一個是用戶不能登錄到iCloud;另一個是程序還不具有支持用戶自動登錄icloud的功能。建議您自己修改一下errorUpdating(_:)方法使之至少能夠處理這些情況。提示:目前這兩類錯誤都返回1(CKErrorCode)。
現在,請構建和運行示例項目。目前,你應該看到一個幾近完整的餐館單位名單列表了。
查詢排錯
如果你正在使用iOS模擬器但卻發現列表控件中沒有填充任何內容,那么,你務必確保從Xcode的菜單項【Debug\Simulate Location\San Francisco, CA, USA】下設置了正確的位置。如果您需要在Xcode中改變這一位置,那么你可以從應用程序的下拉列表中選擇其他位置,從而實現強制刷新而不是被動地等待位置觸發。
如果您使用的是iPhone或iPad且啟用了定位服務而列表仍未填充,那么說明餐館沒有足夠接近你當前的位置。此時,你有兩個選擇:改變樣本數據的坐標以便更接近您的當前位置;或者使用模擬器來運行應用程序。還有第三個選擇,但不是非常實用,即你不得不前往庫比提諾(Cupertino)并進入蘋果公司地區進行試驗。
如果數據未顯示正確——或者根本不顯示,那么,請使用CloudKit控制面板檢查樣本數據。確保所有的記錄都存在,而且你已經將它們添加到默認區域,而且它們的值都是正確的。如果您需要重新輸入數據,那么你可以通過單擊回收站圖標刪除記錄(參考下圖)。
調試CloudKit錯誤有時可能非常棘手。在撰寫本文時,CloudKit錯誤消息中并不包含大量信息。要確定錯誤的原因,您需要查看與你的特定數據庫操作相關連的錯誤代碼。使用數字錯誤代碼,查找相匹配的CKErrorCode枚舉值進行分析。在文檔中的名稱和說明將幫助你縮小問題原因的分析范圍。
【注意】對于可通過CloudKit返回的錯誤代碼列表,請參閱CloudKit框架常量參考(https://developer.apple.com/library/ios/documentation/CloudKit/Reference/CloudKit_constants/#//apple_ref/c/tdef/CKErrorCode)。
下面是一些常見的錯誤枚舉和相關說明:
- BadContainer——指定的容器是未知的或未經授權的。
- NotAuthenticated——當前用戶沒有通過驗證,也沒有用戶記錄可用。如果用戶未登錄到iCloud時可能出現這種情況。
- UnknownItem——指定的記錄不存在。
當你得到餐館的名單列表時,你可能已經注意到了,你可以看到服務餐館名稱及其提供的服務。但沒有顯示圖像!是云端那邊出問題了嗎?
當您檢索服務餐館記錄時,將自動檢索對應的圖像。但是,您仍然需要執行必要的步驟來將圖像加載到你的應用程序中。這需要借助于云端方法了!
使用二進制資源
對于二進制形式的資源數據,如圖像,都有相關聯的記錄。在本文實例中,你的應用程序的資源數據都會顯示于MasterViewController表視圖的餐館照片中。
在本節中,您需要添加邏輯來加載當檢索餐館記錄時已下載的資源。
為此,打開文件Model/Establishment.swift,并用下面的代碼更換loadCoverPhoto(_ :)方法:
- func loadCoverPhoto(completion:(photo: UIImage!) -> ()) {
- // 1
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
- var image: UIImage!
- // 4
- defer {
- completion(photo: image)
- }
- // 2
- guard let asset = self.record["CoverPhoto"] as? CKAsset,
- path = asset.fileURL.path,
- imageData = NSData(contentsOfFile: path) else {
- return
- }
- // 3
- image = UIImage(data: imageData)
- }
- }
此方法從asset有關屬性中加載相關圖片:
1. 雖然你下載了資源,但同時您還檢索了記錄的其余部分數據,因此,你需要使用異步方式加載圖像。如你所見,我們把有關代碼全部封裝在dispatch_async代碼塊內。
2. 資源存儲在CKRecord中,作為CKAsset的實例,所以需要相應地轉換一下。接下來,從資源提供的本地文件URL中加載圖像數據。
3. 使用圖像數據來創建UIImage的一個實例。
4. 執行與檢索的圖像相關的completion回調函數。請注意,不管執行哪一個return語句,延遲(defer)塊都被執行。例如,如果沒有圖像資源,那么,在返回時永遠不會設置image變量的值,當然也就不顯示餐廳對應的圖像。
現在,請構建和運行示例項目。你會注意到,由于上面的云端操作,現在餐館的圖像顯示出來了!
目前,在CloudKit資源中還存在兩個不足:
- 資源只能在CloudKit中作為記錄屬性存在,你不能把它們單獨存儲。刪除記錄也會刪除所有相關資源。
- 因為資源與記錄數據的其余部分在同一時間下載,所以,獲取資源會產生一定的負面性能影響。如果您的應用程序使用了大量的資源,那么,你應該專門存儲一下擁有資源的不同類型記錄的引用。
總結
到現在為止,你應該已經看到了我們的最終項目中所提供的功能。目前,該應用程序已經可以下載餐館記錄,并把它們的詳細資料和照片加載到程序的表視圖中。
實際上,您還可以從以下幾個方面進一步增強本文中的示例應用:
允許用戶添加自己的照片、筆記、評論和投訴。這將有助于他們避免再次遭遇不愉快的經歷。
允許用戶使用地圖創建一個新的餐館記錄。你可以把這樣的功能添加到Model類中,用于把相應記錄保存到公共或私有數據庫中。
添加過濾和搜索支持。工程提供的Model類中已經構建了一個帶有一個距離謂詞的CKQuery,可以把它修改為一個更復雜的謂詞。當然,CloudKit也是支持基于字符串字段的文本搜索的。
提高應用程序的性能和數據加載體驗。你會注意到,本教程中在一切準備就緒時使用了一些工具方法來調用所需的完成處理器函數。另外,CKDatabase的實例也提供了基于NSOperation的方法來更好地控制API執行方式。
為程序提供緩存和同步支持;這樣當應用程序連接到網絡時,它能夠保持線下響應并保持內容***。
使用蘋果公司推出的有著強大后端API支持的CloudKit,相信你能夠把你的應用程序提升到一個更高的水平!