開始用Swift開發(fā)iOS 10 - 22 使用CloudKit

上一篇 開始用Swift開發(fā)iOS 10 - 21 使用WKWebView和SFSafariViewController 學(xué)習(xí)打開網(wǎng)頁,這一篇學(xué)習(xí)使用CloudKit。

iCloud最初喬布斯在WWDC2011是發(fā)布的,只為了給Apps、游戲能夠在云端數(shù)據(jù)保存,讓Mac和iOS之間數(shù)據(jù)自動(dòng)同步所用。
最近幾年才漸漸成長(zhǎng)為云服務(wù)。如果想創(chuàng)建社交類型的,可以分享用戶之間數(shù)據(jù)的app,就可考慮使用iCloud,這樣就不需要自己再去構(gòu)建一個(gè)后端APIs了。雖然可能由于國(guó)內(nèi)的網(wǎng)絡(luò)環(huán)境,iCloud實(shí)際應(yīng)用效果并不好,不過還是有必要學(xué)一下的??。

如果開發(fā)了Web應(yīng)用,同樣也可以訪問iOS應(yīng)用的iCloud中數(shù)據(jù)。Apple分別提供了CloudKit JSCloudKit庫(kù)。

CloudKit默認(rèn)給我們的app提供一些免費(fèi)的空間:

當(dāng)app的用戶的活躍數(shù)提高,免費(fèi)空間也會(huì)隨之提高,詳細(xì)可查看官網(wǎng)介紹。

理解CloudKit框架

CloudKit框架不僅提供了存儲(chǔ)功能,還提供了開發(fā)者與iCloud之間的各種交互。ContainersdatabaseCloudKit框架的基礎(chǔ)元素。

  • 默認(rèn),一個(gè)app就是一個(gè)container,代碼中就是CKContainer,一個(gè)container中包括三個(gè)database(CKDatabase):
    • 一個(gè)public database: app中所有用戶都能查看
    • 一個(gè)shared database:app中一組用戶能查看(iOS 10)
    • 一個(gè)private database:app中單個(gè)用戶查看
Containers and Database
Record zones and Records

為應(yīng)用添加CloudKit

  • 首先需要開發(fā)者賬號(hào)。

  • 然后在Capabilities中打開iCloud。

在CloudKit Dashboard中管理 Record

  • 點(diǎn)擊上圖中的CloudKit Dashboard,或者直接訪問https://icloud.developer.apple.com/dashboard/。最新的CloudKit Dashboard的頁面有了一些變化。首先進(jìn)去的是應(yīng)用列表(也就是container列表),點(diǎn)擊一個(gè)就進(jìn)入如下頁面:
  • 點(diǎn)擊Development的data,類似下面
  • 選擇Record Types(有點(diǎn)像關(guān)系數(shù)據(jù)中的表),創(chuàng)建新的類型Restaurant,并添加幾個(gè)Field的。
  • 選擇Records(類型表中的數(shù)據(jù)),添加幾條數(shù)據(jù),注意選擇public database。

使用 Convenience API獲取public Database

CloudKit提供兩種APIs讓開發(fā)與iCloud交互:the convenience API 和 the operational API。

  • Convenience API的通常調(diào)用方式:

      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()獲取應(yīng)用的Container。
    • publicCloudDatabase表示默認(rèn)的public database。
    • NSPredicateCKQuery是搜索條件
  • 新建DiscoverTableViewController,繼承至UITableViewController,關(guān)聯(lián)discover.storyboard中的table view的控制器; 并修改其prototype cell的identifierCell

  • DiscoverTableViewController.swift中加入import CloudKit,并定義一個(gè)CKRecord的數(shù)組變量:

    var restaurants:[CKRecord] = []
    
  • 添加獲取Records的函數(shù):

      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中,當(dāng)確定獲取到了數(shù)據(jù)后,賦值給restaurants,并刷新table。

  • viewDidLoad中添加: fetchRecordsFromCloud()。

  • 添加table view相關(guān)代理方法:

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值的方法。
  • 圖片對(duì)象轉(zhuǎn)換為CKAsset。

為什么慢?

測(cè)試以上代碼,發(fā)現(xiàn)fetchRecordsFromCloud函數(shù)中的打印信息"Completed the download of Restaurant data"已經(jīng)顯示在控制臺(tái)了,但是還需要過一段時(shí)間App中才能顯示,也就是說向iCloud中獲取完數(shù)據(jù)后才開始準(zhǔn)備table view加載。

這邊就需要使用到多線程的概念。在iOS中,UI更新(像table重新加載)必須在主線程執(zhí)行。這樣獲取iCloud數(shù)據(jù)的線程在進(jìn)行時(shí),UI更新也在同步進(jìn)行。

OperationQueue.main.addOperation {
    self.tableView.reloadData()
}

使用operational API獲取public Database

** Convenience API**只適合簡(jiǎn)單和少量的查詢。

  • 更新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方法,它提供了許多查詢選項(xiàng)。
    • desiredKeys代表需要查詢的字段。
    • resultsLimit代表依次查詢最大Record數(shù)目

加載指示(菊花轉(zhuǎn))

  • 可以在viewDidLoad中添加類型如下代碼:
    let spinner:UIActivityIndicatorView = UIActivityIndicatorView()
    spinner.activityIndicatorViewStyle = .gray
    spinner.center = view.center
    spinner.hidesWhenStopped = true
    view.addSubview(spinner)
    spinner.startAnimating()
    
  • 也可以通過在discover.storyboard中添加:

添加完發(fā)現(xiàn)** activity indicator view在控制器上面,這在Xcode中叫The Extra Views**

DiscoverTableViewController中添加接口,并關(guān)聯(lián)。

  @IBOutlet var spinner: UIActivityIndicatorView!

viewDidLoad中添加代碼:

  spinner.hidesWhenStopped = true
  spinner.center = view.center
  tableView.addSubview(spinner)
  spinner.startAnimating()
  • 數(shù)據(jù)加載完要隱藏加載提示:
    OperationQueue.main.addOperation {
      self.spinner.stopAnimating()
      self.tableView.reloadData()
     }
    

懶加載圖片

懶加載圖片就是先加載一個(gè)本地默認(rèn)圖片,暫時(shí)不加載遠(yuǎn)程圖片,當(dāng)圖片準(zhǔn)備好在去更新圖片視圖。

  • 修改請(qǐng)求字段desireKeys,讓開始時(shí)不加圖片: 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。

懶加載后發(fā)現(xiàn),圖片在其它視圖顯示后慢慢先后加載顯示。

下拉刷新

UIRefreshControl提供了標(biāo)準(zhǔn)的下拉刷新特性。

  • DiscoverTableViewControllerviewDidLoad中添加:
        // Pull To Refresh Control
        refreshControl = UIRefreshControl()
        refreshControl?.backgroundColor = UIColor.white
        refreshControl?.tintColor = UIColor.gray
        refreshControl?.addTarget(self, action: #selector(fetchRecordsFromCloud), for: UIControlEvents.valueChanged)

每一次下拉是顯示菊花轉(zhuǎn),并且調(diào)用fetchRecordsFromCloud方法。

  • fetchRecordsFromCloud方法的queryCompletionBlock添加數(shù)據(jù)加載完成后去除菊花轉(zhuǎn)代碼:
if let refreshControl = self.refreshControl {
    if refreshControl.isRefreshing {
        refreshControl.endRefreshing()
    }
}
  • 刷新會(huì)出現(xiàn)重復(fù)數(shù)據(jù),要在fetchRecordsFromCloud方法開始時(shí),清理數(shù)據(jù):
restaurants.removeAll()
tableView.reloadData()

使用CloudKit保存數(shù)據(jù)到iCloud

CKDatabasesave(_:completionHandler:)的方法可用來保存數(shù)據(jù)到iCloud。
實(shí)現(xiàn)用戶新加數(shù)據(jù)時(shí),既保存在本地的Core Data,有保存在iCloud中。

  • AddRestaurantController中添加:import CloudKit。

  • AddRestaurantController添加方法:

      // 保存到Core Data的同時(shí)也保存的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可用來排序。
DiscoverTableViewControllerfetchRecordsFromCloud方法,query定義后添加:

    query.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]

creationDate是默認(rèn)的創(chuàng)建時(shí)間字段。

Exercise:修改Discover樣式

  • 新建一個(gè)DiscoverTableViewCell,繼承至UITableViewCell,并關(guān)聯(lián)Discover的cell。
  • 修改cell合適的樣式,比如下面
  • DiscoverTableViewCell中新建四個(gè)接口,并關(guān)聯(lián)。
    @IBOutlet var nameLabel: UILabel!
    @IBOutlet var locationLabel: UILabel!
    @IBOutlet var typeLabel: UILabel!
    @IBOutlet var thumbnailImageView: UIImageView!
  • 更新tableView(_:cellForRowAt:)fetchRecordsFromCloud相關(guān)代碼

代碼

Beginning-iOS-Programming-with-Swift

說明

此文是學(xué)習(xí)appcode網(wǎng)站出的一本書 《Beginning iOS 10 Programming with Swift》 的一篇記錄

系列文章目錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容