上一篇 開始用Swift開發iOS 10 - 21 使用WKWebView和SFSafariViewController 學習打開網頁,這一篇學習使用CloudKit。
iCloud最初喬布斯在WWDC2011是發布的,只為了給Apps、游戲能夠在云端數據保存,讓Mac和iOS之間數據自動同步所用。
最近幾年才漸漸成長為云服務。如果想創建社交類型的,可以分享用戶之間數據的app,就可考慮使用iCloud,這樣就不需要自己再去構建一個后端APIs了。雖然可能由于國內的網絡環境,iCloud實際應用效果并不好,不過還是有必要學一下的??。
如果開發了Web應用,同樣也可以訪問iOS應用的iCloud中數據。Apple分別提供了CloudKit JS
和CloudKit
庫。
CloudKit
默認給我們的app提供一些免費的空間:
當app的用戶的活躍數提高,免費空間也會隨之提高,詳細可查看官網介紹。
理解CloudKit框架
CloudKit
框架不僅提供了存儲功能,還提供了開發者與iCloud之間的各種交互。Containers和database是CloudKit
框架的基礎元素。
- 默認,一個app就是一個container,代碼中就是
CKContainer
,一個container中包括三個database(CKDatabase
):- 一個public database: app中所有用戶都能查看
- 一個shared database:app中一組用戶能查看(iOS 10)
- 一個private database:app中單個用戶查看
為應用添加CloudKit
首先需要開發者賬號。
然后在Capabilities中打開iCloud。
在CloudKit Dashboard中管理 Record
- 點擊上圖中的CloudKit Dashboard,或者直接訪問https://icloud.developer.apple.com/dashboard/。最新的CloudKit Dashboard的頁面有了一些變化。首先進去的是應用列表(也就是container列表),點擊一個就進入如下頁面:
- 點擊Development的data,類似下面
- 選擇Record Types(有點像關系數據中的表),創建新的類型Restaurant,并添加幾個Field的。
- 選擇Records(類型表中的數據),添加幾條數據,注意選擇public database。
使用 Convenience API獲取public Database
CloudKit
提供兩種APIs讓開發與iCloud交互:the convenience API 和 the operational API。
-
Convenience API的通常調用方式:
let cloudContainer = CKContainer.default() let publicDatabase = cloudContainer.publicCloudDatabase let predicate = NSPredicate(value: true) let query = CKQuery(recordType: "Restaurant", predicate: predicate) publicDatabase.perform(query, inZoneWith: nil, completionHandler: { (results, error) -> Void in // Process the records })
-
CKContainer.default()
獲取應用的Container。 -
publicCloudDatabase
表示默認的public database。 -
NSPredicate
和CKQuery
是搜索條件
-
新建
DiscoverTableViewController
,繼承至UITableViewController
,關聯discover.storyboard
中的table view的控制器; 并修改其prototype cell的identifier
為Cell
。-
在
DiscoverTableViewController.swift
中加入import CloudKit
,并定義一個CKRecord
的數組變量:var restaurants:[CKRecord] = []
-
添加獲取Records的函數:
func fetchRecordsFromCloud() { let cloudContainer = CKContainer.default() let publicDatabase = cloudContainer.publicCloudDatabase let predicate = NSPredicate(value: true) let query = CKQuery(recordType: "Restaurant", predicate: predicate) publicDatabase.perform(query, inZoneWith: nil, completionHandler: { (results, error) -> Void in if error != nil { print(error) return } if let results = results { print("Completed the download of Restaurant data") self.restaurants = results OperationQueue.main.addOperation { self.spinner.stopAnimating() self.tableView.reloadData() } } }) }
在
perform
中,當確定獲取到了數據后,賦值給restaurants
,并刷新table。 在
viewDidLoad
中添加:fetchRecordsFromCloud()
。添加table view相關代理方法:
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return restaurants.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for:
indexPath)
// Configure the cell...
let restaurant = restaurants[indexPath.row]
cell.textLabel?.text = restaurant.object(forKey: "name") as? String
if let image = restaurant.object(forKey: "image") {
let imageAsset = image as! CKAsset
if let imageData = try? Data.init(contentsOf: imageAsset.fileURL) {
cell.imageView?.image = UIImage(data: imageData)
}
}
return cell
}
-
object(forKey:)
是CKRecord
中獲取Record Field值的方法。 - 圖片對象轉換為
CKAsset
。
為什么慢?
測試以上代碼,發現fetchRecordsFromCloud
函數中的打印信息"Completed the download of Restaurant data"已經顯示在控制臺了,但是還需要過一段時間App中才能顯示,也就是說向iCloud中獲取完數據后才開始準備table view加載。
這邊就需要使用到多線程的概念。在iOS中,UI更新(像table重新加載)必須在主線程執行。這樣獲取iCloud數據的線程在進行時,UI更新也在同步進行。
OperationQueue.main.addOperation {
self.tableView.reloadData()
}
使用operational API獲取public Database
** Convenience API**只適合簡單和少量的查詢。
- 更新
fetchRecordsFromCloud
方法:func fetchRecordsFromCloud() { let cloudContainer = CKContainer.default() let publicDatabase = cloudContainer.publicCloudDatabase let predicate = NSPredicate(value: true) let query = CKQuery(recordType: "Restaurant", predicate: predicate) // Create the query operation with the query let queryOperation = CKQueryOperation(query: query) queryOperation.desiredKeys = ["name", "image"] queryOperation.queuePriority = .veryHigh queryOperation.resultsLimit = 50 queryOperation.recordFetchedBlock = { (record) -> Void in self.restaurants.append(record) } queryOperation.queryCompletionBlock = { (cursor, error) -> Void in if let error = error { print("Failed to get data from iCloud - \(error.localizedDescription)") return } print("Successfully retrieve the data from iCloud") OperationQueue.main.addOperation { self.tableView.reloadData() } } // Execute the query publicDatabase.add(queryOperation) }
- 通過
CKQueryOperation
代替perform
方法,它提供了許多查詢選項。 -
desiredKeys
代表需要查詢的字段。 -
resultsLimit
代表依次查詢最大Record數目
- 通過
加載指示(菊花轉)
- 可以在
viewDidLoad
中添加類型如下代碼:let spinner:UIActivityIndicatorView = UIActivityIndicatorView() spinner.activityIndicatorViewStyle = .gray spinner.center = view.center spinner.hidesWhenStopped = true view.addSubview(spinner) spinner.startAnimating()
- 也可以通過在
discover.storyboard
中添加:
添加完發現** activity indicator view在控制器上面,這在Xcode中叫The Extra Views**
在DiscoverTableViewController
中添加接口,并關聯。
@IBOutlet var spinner: UIActivityIndicatorView!
在viewDidLoad
中添加代碼:
spinner.hidesWhenStopped = true
spinner.center = view.center
tableView.addSubview(spinner)
spinner.startAnimating()
- 數據加載完要隱藏加載提示:
OperationQueue.main.addOperation { self.spinner.stopAnimating() self.tableView.reloadData() }
懶加載圖片
懶加載圖片就是先加載一個本地默認圖片,暫時不加載遠程圖片,當圖片準備好在去更新圖片視圖。
修改請求字段
desireKeys
,讓開始時不加圖片:queryOperation.desiredKeys = ["name"]
更新
tableView(_:cellForRowAt:)
:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let restaurant = restaurants[indexPath.row]
cell.textLabel?.text = restaurant.object(forKey: "name") as? String
// Set the default image
cell.imageView?.image = UIImage(named: "photoalbum")
// Fetch Image from Cloud in background
let publicDatabase = CKContainer.default().publicCloudDatabase
let fetchRecordsImageOperation = CKFetchRecordsOperation(recordIDs:[restaurant.recordID])
fetchRecordsImageOperation.desiredKeys = ["image"]
fetchRecordsImageOperation.queuePriority = .veryHigh
fetchRecordsImageOperation.perRecordCompletionBlock = { (record, recordID, error) -> Void in
if let error = error {
print("Failed to get restaurant image: \(error.localizedDescription)")
return
}
if let restaurantRecord = record {
OperationQueue.main.addOperation() {
if let image = restaurantRecord.object(forKey: "image") {
let imageAsset = image as! CKAsset
print(imageAsset.fileURL)
if let imageData = try? Data.init(contentsOf:
imageAsset.fileURL) {
cell.imageView?.image = UIImage(data: imageData)
}
}
}
}
}
publicDatabase.add(fetchRecordsImageOperation)
return cell
}
-
CKFetchRecordsOperation
通過recordID
獲得特定的Record。 -
CKFetchRecordsOperation
一些方法類似CKQueryOperation
。
懶加載后發現,圖片在其它視圖顯示后慢慢先后加載顯示。
下拉刷新
UIRefreshControl
提供了標準的下拉刷新特性。
- 在
DiscoverTableViewController
的viewDidLoad
中添加:
// Pull To Refresh Control
refreshControl = UIRefreshControl()
refreshControl?.backgroundColor = UIColor.white
refreshControl?.tintColor = UIColor.gray
refreshControl?.addTarget(self, action: #selector(fetchRecordsFromCloud), for: UIControlEvents.valueChanged)
每一次下拉是顯示菊花轉,并且調用fetchRecordsFromCloud
方法。
- 在
fetchRecordsFromCloud
方法的queryCompletionBlock
添加數據加載完成后去除菊花轉代碼:
if let refreshControl = self.refreshControl {
if refreshControl.isRefreshing {
refreshControl.endRefreshing()
}
}
- 刷新會出現重復數據,要在
fetchRecordsFromCloud
方法開始時,清理數據:
restaurants.removeAll()
tableView.reloadData()
使用CloudKit保存數據到iCloud
CKDatabase
的save(_:completionHandler:)
的方法可用來保存數據到iCloud。
實現用戶新加數據時,既保存在本地的Core Data,有保存在iCloud中。
在
AddRestaurantController
中添加:import CloudKit
。-
在
AddRestaurantController
添加方法:// 保存到Core Data的同時也保存的iCloud中 func saveRecordToCloud(restaurant:RestaurantMO!) -> Void { // Prepare the record to save let record = CKRecord(recordType: "Restaurant") record.setValue(restaurant.name, forKey: "name") record.setValue(restaurant.type, forKey: "type") record.setValue(restaurant.location, forKey: "location") record.setValue(restaurant.phone, forKey: "phone") let imageData = restaurant.image as! Data // Resize the image let originalImage = UIImage(data: imageData)! let scalingFactor = (originalImage.size.width > 1024) ? 1024 / originalImage.size.width : 1.0 let scaledImage = UIImage(data: imageData, scale: scalingFactor)! // Write the image to local file for temporary use let imageFilePath = NSTemporaryDirectory() + restaurant.name! let imageFileURL = URL(fileURLWithPath: imageFilePath) try? UIImageJPEGRepresentation(scaledImage, 0.8)?.write(to: imageFileURL) // Create image asset for upload let imageAsset = CKAsset(fileURL: imageFileURL) record.setValue(imageAsset, forKey: "image") // Get the Public iCloud Database let publicDatabase = CKContainer.default().publicCloudDatabase // Save the record to iCloud publicDatabase.save(record, completionHandler: { (record, error) -> Void in try? FileManager.default.removeItem(at: imageFileURL) }) }
-
在
save
方法的dismiss(animated:completion:)
的前面添加:saveRecordToCloud(restaurant: restaurant)
排序
CKQuery
有屬性sortDescriptors
可用來排序。
在DiscoverTableViewController
的fetchRecordsFromCloud
方法,query定義后添加:
query.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
creationDate
是默認的創建時間字段。
Exercise:修改Discover樣式
- 新建一個
DiscoverTableViewCell
,繼承至UITableViewCell
,并關聯Discover的cell。 - 修改cell合適的樣式,比如下面
- 在
DiscoverTableViewCell
中新建四個接口,并關聯。
@IBOutlet var nameLabel: UILabel!
@IBOutlet var locationLabel: UILabel!
@IBOutlet var typeLabel: UILabel!
@IBOutlet var thumbnailImageView: UIImageView!
- 更新
tableView(_:cellForRowAt:)
和fetchRecordsFromCloud
相關代碼
代碼
Beginning-iOS-Programming-with-Swift
說明
此文是學習appcode網站出的一本書 《Beginning iOS 10 Programming with Swift》 的一篇記錄
系列文章目錄
- 開始用Swift開發iOS 10 - 1 前言
- 開始用Swift開發iOS 10 - 2 Hello World!第一個Swift APP
- 開始用Swift開發iOS 10 - 3 介紹Auto Layout
- 開始用Swift開發iOS 10 - 4 用Stack View設計UI
- [開始用Swift開發iOS 10 - 5 原型的介紹]
- 開始用Swift開發iOS 10 - 6 創建簡單的Table Based App
- 開始用Swift開發iOS 10 - 7 定制Table Views
- 開始用Swift開發iOS 10 - 8 Table View和UIAlertController的交互
- 開始用Swift開發iOS 10 - 9 Table Row的刪除, UITableViewRowAction和UIActivityViewController的使用
- 開始用Swift開發iOS 10 - 10 Navigation Controller的介紹和Segue
- 開始用Swift開發iOS 10 - 11 面向對象編程介紹
- 開始用Swift開發iOS 10 - 12 豐富Detail View和定制化Navigation Bar
- 開始用Swift開發iOS 10 - 13 Self Sizing Cells and Dynamic Type
- 開始用Swift開發iOS 10 - 14 基礎動畫,模糊效果和Unwind Segue
- 開始用Swift開發iOS 10 - 15 使用地圖
- 開始用Swift開發iOS 10 - 16 介紹靜態Table Views,UIImagePickerController和NSLayoutConstraint
- 開始用Swift開發iOS 10 - 17 使用Core Data