CloudKit框架详细解析(三) —— CloudKit一个基本使用示例(二)

版本记录

版本号 时间
V1.0 2019.10.21 星期一

前言

将结构化app和用户数据存储在可由应用程序的所有用户共享的iCloud容器中。感兴趣的可以看下面几篇文章。
1. CloudKit框架详细解析(一) —— 基本概览(一)
2. CloudKit框架详细解析(二) —— CloudKit一个基本使用示例(一)

源码

1. Swift

首先看下项目组织结构

接着就是看sb中的内容

下面就是源码了

1. Model.swift
import Foundation
import CloudKit

class Model {
  // MARK: - iCloud Info
  let container: CKContainer
  let publicDB: CKDatabase
  let privateDB: CKDatabase
  
  // MARK: - Properties
  private(set) var establishments: [Establishment] = []
  static var currentModel = Model()
  
  init() {
    container = CKContainer.default()
    publicDB = container.publicCloudDatabase
    privateDB = container.privateCloudDatabase
  }
  
  @objc func refresh(_ completion: @escaping (Error?) -> Void) {
    let predicate = NSPredicate(value: true)
    let query = CKQuery(recordType: "Establishment", predicate: predicate)
    establishments(forQuery: query, completion)
  }

  
  private func establishments(forQuery query: CKQuery, _ completion: @escaping (Error?) -> Void) {
    publicDB.perform(query, inZoneWith: CKRecordZone.default().zoneID) { [weak self] results, error in
      guard let self = self else { return }
      if let error = error {
        DispatchQueue.main.async {
          completion(error)
        }
        return
      }
      guard let results = results else { return }
      self.establishments = results.compactMap {
        Establishment(record: $0, database: self.publicDB)
      }
      DispatchQueue.main.async {
        completion(nil)
      }
    }
  }
}
2. Establishment.swift
import UIKit
import MapKit
import CloudKit
import CoreLocation

class Establishment {
  enum ChangingTable: Int {
    case none
    case womens
    case mens
    case both
  }
  
  static let recordType = "Establishment"
  private let id: CKRecord.ID
  let name: String
  let location: CLLocation
  let coverPhoto: CKAsset?
  let database: CKDatabase
  let changingTable: ChangingTable
  let kidsMenu: Bool
  let healthyOption: Bool
  private(set) var notes: [Note]? = nil
  
  init?(record: CKRecord, database: CKDatabase) {
    guard
      let name = record["name"] as? String,
      let location = record["location"] as? CLLocation
      else { return nil }
    id = record.recordID
    self.name = name
    self.location = location
    coverPhoto = record["coverPhoto"] as? CKAsset
    self.database = database
    healthyOption = record["healthyOption"] as? Bool ?? false
    kidsMenu = record["kidsMenu"] as? Bool ?? false
    if let changingTableValue = record["changingTable"] as? Int,
      let changingTable = ChangingTable(rawValue: changingTableValue) {
      self.changingTable = changingTable
    } else {
      self.changingTable = .none
    }
    if let noteRecords = record["notes"] as? [CKRecord.Reference] {
      Note.fetchNotes(for: noteRecords) { notes in
        self.notes = notes
      }
    }
  }
  
  func loadCoverPhoto(completion: @escaping (_ photo: UIImage?) -> ()) {
    DispatchQueue.global(qos: .utility).async {
      var image: UIImage?
      defer {
        DispatchQueue.main.async {
          completion(image)
        }
      }
      guard
        let coverPhoto = self.coverPhoto,
        let fileURL = coverPhoto.fileURL
        else {
          return
      }
      let imageData: Data
      do {
        imageData = try Data(contentsOf: fileURL)
      } catch {
        return
      }
      image = UIImage(data: imageData)
    }
  }
}

extension Establishment: Hashable {
  static func == (lhs: Establishment, rhs: Establishment) -> Bool {
    return lhs.id == rhs.id
  }
  
  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
}
3. Note.swift
import Foundation
import CloudKit

class Note {
  private let id: CKRecord.ID
  private(set) var noteLabel: String?
  let establishmentReference: CKRecord.Reference?

  init(record: CKRecord) {
    id = record.recordID
    noteLabel = record["text"] as? String
    establishmentReference = record["establishment"] as? CKRecord.Reference
  }
  
  static func fetchNotes(_ completion: @escaping (Result<[Note], Error>) -> Void) {
    let query = CKQuery(recordType: "Note",
                        predicate: NSPredicate(value: true))
    let container = CKContainer.default()
    
    container.privateCloudDatabase.perform(query, inZoneWith: nil) { results, error in
      if let error = error {
        DispatchQueue.main.async {
          completion(.failure(error))
        }
        return
      }
        
      guard let results = results else {
        DispatchQueue.main.async {
          let error = NSError(
            domain: "com.babifud", code: -1,
            userInfo: [NSLocalizedDescriptionKey:
                       "Could not download notes"])
          completion(.failure(error))
        }
        return
      }
      
      let notes = results.map(Note.init)
      DispatchQueue.main.async {
        completion(.success(notes))
      }
    }
  }
  
  static func fetchNotes(for references: [CKRecord.Reference], _ completion: @escaping ([Note]) -> Void) {
    let recordIDs = references.map { $0.recordID }
    let operation = CKFetchRecordsOperation(recordIDs: recordIDs)
    operation.qualityOfService = .utility
    
    operation.fetchRecordsCompletionBlock = { records, error in
      let notes = records?.values.map(Note.init) ?? []
      DispatchQueue.main.async {
        completion(notes)
      }
    }
    
    Model.currentModel.privateDB.add(operation)
  }
}

extension Note: Hashable {
  static func == (lhs: Note, rhs: Note) -> Bool {
    return lhs.id == rhs.id
  }
  
  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
}
4. NearbyCell.swift
import UIKit

class NearbyCell: UITableViewCell {
  // MARK: - Outlets
  @IBOutlet private weak var placeImageView: UIImageView!
  @IBOutlet private weak var name: UILabel!

  var establishment: Establishment? {
    didSet {
      placeImageView.image = nil
      name.text = establishment?.name
      establishment?.loadCoverPhoto { [weak self] image in
        guard let self = self else { return }
        self.placeImageView.image = image
      }
    }
  }
}
5. NearbyTableViewController.swift
import UIKit
import CoreLocation

class NearbyTableViewController: UITableViewController {
  var locationManager: CLLocationManager!
  var dataSource: UITableViewDiffableDataSource?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    setupLocationManager()
    dataSource = establishmentDataSource()
    tableView.dataSource = dataSource
    refreshControl = UIRefreshControl()
    refreshControl?.addTarget(self, action: #selector(refresh), for: .valueChanged)
    refresh()
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    reloadSnapshot(animated: false)
  }
  
  @objc private func refresh() {
    Model.currentModel.refresh { error in
      if let error = error {
        let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        self.present(alert, animated: true, completion: nil)
        self.tableView.refreshControl?.endRefreshing()
        return
      }
      self.tableView.refreshControl?.endRefreshing()
      self.reloadSnapshot(animated: true)
    }
  }

  // MARK: - Navigation
  @IBSegueAction private func detailSegue(coder: NSCoder, sender: Any?) -> DetailTableViewController? {
    guard
      let cell = sender as? NearbyCell,
      let indexPath = tableView.indexPath(for: cell),
      let detailViewController = DetailTableViewController(coder: coder)
      else { return nil }
    detailViewController.establishment = Model.currentModel.establishments[indexPath.row]
    return detailViewController
  }
  
}

extension NearbyTableViewController {
  private func establishmentDataSource() -> UITableViewDiffableDataSource {
    let reuseIdentifier = "NearbyCell"
    return UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, establishment) -> NearbyCell? in
      let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as? NearbyCell
      cell?.establishment = establishment
      return cell
    }
  }
  
  private func reloadSnapshot(animated: Bool) {
    var snapshot = NSDiffableDataSourceSnapshot()
    snapshot.appendSections([0])
    snapshot.appendItems(Model.currentModel.establishments)
    dataSource?.apply(snapshot, animatingDifferences: animated)
    if Model.currentModel.establishments.isEmpty {
      let label = UILabel()
      label.text = "No Restaurants Found"
      label.textColor = UIColor.systemGray2
      label.textAlignment = .center
      label.font = UIFont.preferredFont(forTextStyle: .title2)
      tableView.backgroundView = label
    } else {
      tableView.backgroundView = nil
    }
  }
}

// MARK: - CLLocationManagerDelegate
extension NearbyTableViewController: CLLocationManagerDelegate {
  func setupLocationManager() {
    locationManager = CLLocationManager()
    locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
    
    // Only look at locations within a 0.5 km radius.
    locationManager.distanceFilter = 500.0
    locationManager.delegate = self
    
    CLLocationManager.authorizationStatus()
  }
  
  func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus)  {
    switch status {
    case .notDetermined:
      manager.requestWhenInUseAuthorization()
    case .authorizedWhenInUse:
      manager.startUpdatingLocation()
    default:
      // Do nothing.
      print("Other status")
    }
  }
}
6. DetailTableViewController.swift
import UIKit

class DetailTableViewController: UITableViewController {
  // MARK: - Outlets
  @IBOutlet private weak var imageView: UIImageView!
  @IBOutlet private weak var kidsMenuImageView: UIImageView!
  @IBOutlet private weak var healthyOptionImageView: UIImageView!
  @IBOutlet private weak var womensChangingLabel: UILabel!
  @IBOutlet private weak var mensChangingLabel: UILabel!
  
  // MARK: - Properties
  var establishment: Establishment?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    setup()
  }
  
  private func setup() {
    guard let establishment = establishment else { return }
    title = establishment.name
    let circleFill = "checkmark.circle.fill"
    let notAvailable = "xmark.circle"
    var kidsImageName = circleFill
    var healthyFoodName = circleFill
    if !establishment.kidsMenu {
      kidsImageName = notAvailable
    }
    if !establishment.healthyOption {
      healthyFoodName = notAvailable
    }
    kidsMenuImageView.image = UIImage(systemName: kidsImageName)
    healthyOptionImageView.image = UIImage(systemName: healthyFoodName)
    let changingTable = establishment.changingTable
    womensChangingLabel.alpha = (changingTable == .womens || changingTable == .both) ? 1.0 : 0.5
    mensChangingLabel.alpha = (changingTable == .mens || changingTable == .both) ? 1.0 : 0.5
    establishment.loadCoverPhoto { [weak self] image in
      guard let self = self else { return }
      self.imageView.image = image
    }
  }
  
  // MARK: - Navigation
  @IBSegueAction private func notesSegue(coder: NSCoder, sender: Any?) -> NotesTableViewController? {
    guard let notesTableViewController = NotesTableViewController(coder: coder) else { return nil }
    notesTableViewController.establishment = establishment
    return notesTableViewController
  }
}
7. NotesTableViewController.swift
import UIKit
import CloudKit

class NotesTableViewController: UITableViewController {
  // MARK: - Properties
  var notes: [Note] = []
  var establishment: Establishment?
  var dataSource: UITableViewDiffableDataSource?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    dataSource = notesDataSource()
    if establishment == nil {
      refreshControl = UIRefreshControl()
      refreshControl?.addTarget(self, action: #selector(refresh), for: .valueChanged)
      refresh()
    }
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    reloadSnapshot(animated: false)
  }
  
  @objc private func refresh() {
    Note.fetchNotes { result in
      self.refreshControl?.endRefreshing()
      switch result {
      case .failure(let error):
        let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
        self.present(alert, animated: true, completion: nil)
      case .success(let notes):
        self.notes = notes
      }
      self.reloadSnapshot(animated: true)
    }
  }
}

extension NotesTableViewController {
  private func notesDataSource() -> UITableViewDiffableDataSource {
    let reuseIdentifier = "NoteCell"
    return UITableViewDiffableDataSource(tableView: tableView) { (tableView, indexPath, note) -> UITableViewCell? in
      let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
      cell.textLabel?.text = note.noteLabel
      return cell
    }
  }
  
  private func reloadSnapshot(animated: Bool) {
    var snapshot = NSDiffableDataSourceSnapshot()
    snapshot.appendSections([0])
    var isEmpty = false
    if let establishment = establishment {
      snapshot.appendItems(establishment.notes ?? [])
      isEmpty = (establishment.notes ?? []).isEmpty
    } else {
      snapshot.appendItems(notes)
      isEmpty = notes.isEmpty
    }
    dataSource?.apply(snapshot, animatingDifferences: animated)
    if isEmpty {
      let label = UILabel()
      label.text = "No Notes Found"
      label.textColor = UIColor.systemGray2
      label.textAlignment = .center
      label.font = UIFont.preferredFont(forTextStyle: .title2)
      tableView.backgroundView = label
    } else {
      tableView.backgroundView = nil
    }
  }
}

后记

本篇主要讲述了CloudKit一个基本使用示例,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(CloudKit框架详细解析(三) —— CloudKit一个基本使用示例(二))