开始用Swift开发iOS 10 - 22 使用CloudKit

上一篇 开始用Swift开发iOS 10 - 21 使用WKWebView和SFSafariViewController 学习打开网页,这一篇学习使用CloudKit。

iCloud最初乔布斯在WWDC2011是发布的,只为了给Apps、游戏能够在云端数据保存,让Mac和iOS之间数据自动同步所用。
最近几年才渐渐成长为云服务。如果想创建社交类型的,可以分享用户之间数据的app,就可考虑使用iCloud,这样就不需要自己再去构建一个后端APIs了。虽然可能由于国内的网络环境,iCloud实际应用效果并不好,不过还是有必要学一下的🙂。

如果开发了Web应用,同样也可以访问iOS应用的iCloud中数据。Apple分别提供了CloudKit JSCloudKit库。

CloudKit默认给我们的app提供一些免费的空间:

当app的用户的活跃数提高,免费空间也会随之提高,详细可查看官网介绍

理解CloudKit框架

CloudKit框架不仅提供了存储功能,还提供了开发者与iCloud之间的各种交互。ContainersdatabaseCloudKit框架的基础元素。

  • 默认,一个app就是一个container,代码中就是CKContainer,一个container中包括三个database(CKDatabase):
    • 一个public database: app中所有用户都能查看
    • 一个shared database:app中一组用户能查看(iOS 10)
    • 一个private database:app中单个用户查看
Containers and Database
Record zones and Records

为应用添加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。
    • NSPredicateCKQuery是搜索条件
  • 新建DiscoverTableViewController,继承至UITableViewController,关联discover.storyboard中的table view的控制器; 并修改其prototype cell的identifierCell

  • 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提供了标准的下拉刷新特性。

  • DiscoverTableViewControllerviewDidLoad中添加:
        // 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

CKDatabasesave(_: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可用来排序。
DiscoverTableViewControllerfetchRecordsFromCloud方法,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》 的一篇记录

系列文章目录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容