数据持久化方案解析(四) —— 基于NSCoding的持久化存储(二)

版本记录

版本号 时间
V1.0 2018.10.26 星期五

前言

数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说plist文件(属性列表)、preference(偏好设置)、NSKeyedArchiver(归档)、SQLite 3CoreData,这里基本上我们都用过。这几种方案各有优缺点,其中,CoreData是苹果极力推荐我们使用的一种方式,我已经将它分离出去一个专题进行说明讲解。这个专题主要就是针对另外几种数据持久化存储方案而设立。
1. 数据持久化方案解析(一) —— 一个简单的基于SQLite持久化方案示例(一)
2. 数据持久化方案解析(二) —— 一个简单的基于SQLite持久化方案示例(二)
3. 数据持久化方案解析(三) —— 基于NSCoding的持久化存储(一)

源码

1. Swift

首先看一下代码组织结构。

接着看一下sb中的内容

下面就是源码部分了

1. MasterViewController.swift
import UIKit

class MasterViewController: UITableViewController {
  var creatures: [ScaryCreatureDoc] = []
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    navigationItem.leftBarButtonItem = editButtonItem
    
    let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addTapped(_:)))
    navigationItem.rightBarButtonItem = addButton
    
    title = "Scary Creatures"
    
    loadCreatures()
  }
  
  // MARK: - Segues
  
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "showDetail" {
      if let indexPath = tableView.indexPathForSelectedRow {
        let object = creatures[indexPath.row]
        let controller = segue.destination as! DetailViewController
        controller.detailItem = object
      }
    }
  }
  
  override func didMove(toParent parent: UIViewController?) {
    tableView.reloadData()
  }
  
  // MARK: - Preloading Data
  func loadCreatures() {
//    let creature1 = ScaryCreatureDoc(title: "Ghost", rating: 5, thumbImage: #imageLiteral(resourceName: "ghostThumb"), fullImage: #imageLiteral(resourceName: "ghost"))
//    let creature2 = ScaryCreatureDoc(title: "Monster", rating: 5, thumbImage: #imageLiteral(resourceName: "monsterThumb"), fullImage: #imageLiteral(resourceName: "monster"))
//    let creature3 = ScaryCreatureDoc(title: "Panda", rating: 1, thumbImage: #imageLiteral(resourceName: "pandaThumb"), fullImage: #imageLiteral(resourceName: "panda"))
//    let creature4 = ScaryCreatureDoc(title: "Red Bug", rating: 3, thumbImage: #imageLiteral(resourceName: "redBugThumb"), fullImage: #imageLiteral(resourceName: "redBug"))
//    let creature5 = ScaryCreatureDoc(title: "Slug", rating: 4, thumbImage: #imageLiteral(resourceName: "slugThumb"), fullImage: #imageLiteral(resourceName: "slug"))
//    let creature6 = ScaryCreatureDoc(title: "Spider", rating: 3, thumbImage: #imageLiteral(resourceName: "spiderThumb"), fullImage: #imageLiteral(resourceName: "spider"))
//    let creature7 = ScaryCreatureDoc(title: "Yeti", rating: 3, thumbImage: #imageLiteral(resourceName: "yetiThumb"), fullImage: #imageLiteral(resourceName: "yeti"))
//
//    creatures = [creature1, creature2, creature3, creature4, creature5, creature6, creature7]
    
    creatures = ScaryCreatureDatabase.loadScaryCreatureDocs()
  }
  
  // MARK: - Table View
  
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return creatures.count
  }
  
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "MyBasicCell", for: indexPath)
    
    let creature = creatures[indexPath.row]
    cell.textLabel!.text = creature.data?.title
    cell.imageView!.image = creature.thumbImage
    
    return cell
  }
  
  override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
  }
  
  override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
      let creatureToDelete = creatures.remove(at: indexPath.row)
      creatureToDelete.deleteDoc()
      tableView.deleteRows(at: [indexPath], with: .fade)
    }
  }
  
  // MARK: - IBActions
  
  @objc func addTapped(_ sender: Any) {
    let newDoc = ScaryCreatureDoc(title: "New Creature", rating: 0, thumbImage: nil, fullImage: nil)
    creatures.append(newDoc)
    
    let newIndexPath = IndexPath(row: creatures.count - 1, section: 0)
    tableView.insertRows(at: [newIndexPath], with: .automatic)
    tableView.selectRow(at: newIndexPath, animated: true, scrollPosition: .middle)
    performSegue(withIdentifier: "showDetail", sender: self)
  }
}
2. DetailViewController.swift
import UIKit

class DetailViewController: UIViewController {
  @IBOutlet weak var rateView: RateView!
  @IBOutlet weak var detailDescriptionLabel: UILabel!
  @IBOutlet weak var titleField: UITextField!
  @IBOutlet weak var imageView: UIImageView!
  
  private var picker: UIImagePickerController!
  
  var detailItem: ScaryCreatureDoc? {
    didSet {
      if isViewLoaded {
        configureView()
      }
    }
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    picker = UIImagePickerController()
    configurePicker()
    configureView()
  }
  
  func configurePicker() {
    picker.delegate = self
    picker.sourceType = .photoLibrary
    picker.allowsEditing = false
  }
  
  func configureView() {
    rateView.notSelectedImage = #imageLiteral(resourceName: "shockedface2_empty")
    rateView.fullSelectedImage = #imageLiteral(resourceName: "shockedface2_full")
    rateView.editable = true
    rateView.maxRating = 5
    rateView.delegate = self
    
    if let detailItem = detailItem {
      titleField.text = detailItem.data!.title
      rateView.rating = detailItem.data!.rating
      imageView.image = detailItem.fullImage
      detailDescriptionLabel.isHidden = imageView.image != nil
    }
  }
  
  @IBAction func addPictureTapped(_ sender: UIButton) {
    present(picker, animated: true, completion: nil)
  }
  
  @IBAction func titleFieldTextChanged(_ sender: UITextField) {
    detailItem?.data?.title = sender.text!
    detailItem?.saveData()
  }
}

// MARK: - RateViewDelegate

extension DetailViewController: RateViewDelegate {
  func rateViewRatingDidChange(rateView: RateView, newRating: Float) {
    detailItem?.data?.rating = newRating
    detailItem?.saveData()
  }
}

// MARK: - UIImagePickerControllerDelegate

extension DetailViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
  func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    dismiss(animated: true, completion: nil)
  }
  
  func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
    let fullImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
    let concurrentQueue = DispatchQueue(label: "ResizingQueue", attributes: .concurrent)
    
    concurrentQueue.async {
      let thumbImage = fullImage.resized(newSize: CGSize(width: 107, height: 107))
      
      DispatchQueue.main.async {
        self.detailItem?.fullImage = fullImage
        self.detailItem?.thumbImage = thumbImage
        self.imageView.image = fullImage
        self.detailItem?.saveImages()
      }
    }
    dismiss(animated: true, completion: nil)
  }
}

// MARK: - UITextFieldDelegate

extension DetailViewController: UITextFieldDelegate {
  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    return true
  }
}
3. Extensions.swift
import UIKit

extension UIImage {
  func resized(newSize: CGSize) -> UIImage {
    let horizontalRatio = newSize.width / size.width
    let verticalRatio = newSize.height / size.height
    
    let ratio = max(horizontalRatio, verticalRatio)
    
    return resized(ratio: ratio)
  }
  
  func resized(ratio: CGFloat) -> UIImage {
    let newSize = CGSize(width: size.width * ratio, height: size.height * ratio)
    UIGraphicsBeginImageContextWithOptions(newSize, true, 0)
    draw(in: CGRect(origin: .zero, size: newSize))
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return newImage!
  }
}
4. RateView.swift
import UIKit

protocol RateViewDelegate {
  func rateViewRatingDidChange(rateView: RateView, newRating: Float)
}

class RateView: UIView {
  var notSelectedImage: UIImage? {
    didSet {
      refresh()
    }
  }
  
  var fullSelectedImage: UIImage? {
    didSet {
      refresh()
    }
  }
  
  var rating: Float = 0 {
    didSet {
      refresh()
    }
  }
  
  var editable = false
  var imageViews: [UIImageView] = []
  var maxRating = 5 {
    didSet {
      rebindMaxRating()
    }
  }
  
  var midMargin: CGFloat = 5
  var leftMargin: CGFloat = 0
  var minImageSize = CGSize(width: 5, height: 5)
  var delegate: RateViewDelegate!
  
  private func refresh() {
    for (i, imageView) in imageViews.enumerated() {
      if (rating >= Float(i + 1)) {
        imageView.image = fullSelectedImage;
      } else {
        imageView.image = notSelectedImage;
      }
    }
  }
  
  override func layoutSubviews() {
    super.layoutSubviews()
    
    guard notSelectedImage != nil else { return }
    
    let desiredImageWidth = (frame.width - (leftMargin * 2) - (midMargin * CGFloat(imageViews.count))) / CGFloat(imageViews.count)
    let imageWidth = max(minImageSize.width, desiredImageWidth)
    let imageHeight = max(minImageSize.height, frame.height);
    
    for (i, imageView) in imageViews.enumerated() {
      let imageFrame = CGRect(x: leftMargin + (CGFloat(i) * (midMargin + imageWidth)), y: 0, width: imageWidth, height: imageHeight)
      imageView.frame = imageFrame
    }
  }
  
  private func rebindMaxRating() {
    imageViews.forEach { $0.removeFromSuperview() }
    imageViews.removeAll()
    
    for _ in 0.. imageView.frame.minX {
        newRating = i + 1
        break;
      }
    }
    
    rating = Float(newRating)
  }
  
  override func touchesBegan(_ touches: Set, with event: UIEvent?) {
    if let touch = touches.first {
      let touchLocation = touch.location(in: self)
      handleTouch(touchLocation: touchLocation)
    }
  }
  
  override func touchesMoved(_ touches: Set, with event: UIEvent?) {
    if let touch = touches.first {
      let touchLocation = touch.location(in: self)
      handleTouch(touchLocation: touchLocation)
    }
  }
  
  override func touchesEnded(_ touches: Set, with event: UIEvent?) {
    delegate.rateViewRatingDidChange(rateView: self, newRating: rating)
  }
}
5. ScaryCreatureData.swift
import Foundation

class ScaryCreatureData: NSObject, NSCoding, NSSecureCoding {
  var title = ""
  var rating: Float = 0
  
  init(title: String, rating: Float) {
    super.init()
    self.title = title
    self.rating = rating
  }
  
  // MARK: NSCoding Implementation
  
  enum Keys: String {
    case title = "Title"
    case rating = "Rating"
  }
  
  func encode(with aCoder: NSCoder) {
//    For NSCoding
    aCoder.encode(title, forKey: Keys.title.rawValue)
    aCoder.encode(rating, forKey: Keys.rating.rawValue)
    
//    For NSSecureCoding
//    aCoder.encode(title as NSString, forKey: Keys.title.rawValue)
//    aCoder.encode(NSNumber(value: rating), forKey: Keys.rating.rawValue)
  }
  
  required convenience init?(coder aDecoder: NSCoder) {
//    For NSCoding
//    let title = aDecoder.decodeObject(forKey: Keys.title.rawValue) as! String
//    let rating = aDecoder.decodeFloat(forKey: Keys.rating.rawValue)
    
//    For NSSecureCoding
    let title = aDecoder.decodeObject(of: NSString.self, forKey: Keys.title.rawValue) as String? ?? ""
    let rating = aDecoder.decodeObject(of: NSNumber.self, forKey: Keys.rating.rawValue)
    self.init(title: title, rating: rating?.floatValue ?? 0)
  }
  
  static var supportsSecureCoding: Bool {
    return true
  }
}
6. ScaryCreatureDoc.swift
import UIKit

class ScaryCreatureDoc: NSObject {
  enum Keys: String {
    case dataFile = "Data.plist"
    case thumbImageFile = "thumbImage.png"
    case fullImageFile = "fullImage.png"
  }
  
  private var _data: ScaryCreatureData?
  var data: ScaryCreatureData? {
    get {
      // 1) return the value if already loaded
      if _data != nil { return _data }
      
      // 2) read the saved file as 'Data'
      let dataURL = docPath!.appendingPathComponent(Keys.dataFile.rawValue)
      guard let codedData = try? Data(contentsOf: dataURL) else { return nil }
//      For NSCoding
      // 3) unarchive the object from the Data object
      _data = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(codedData) as? ScaryCreatureData
      
//      For NSSecureCoding
      
//      _data = try! NSKeyedUnarchiver.unarchivedObject(ofClass: ScaryCreatureData.self, from: codedData)
      return _data
    }
    set {
      _data = newValue
    }
  }
  
  private var _thumbImage: UIImage?
  var thumbImage: UIImage? {
    get {
      if _thumbImage != nil { return _thumbImage }
      if docPath == nil { return nil }
      
      let thumbImageURL = docPath!.appendingPathComponent(Keys.thumbImageFile.rawValue)
      guard let imageData = try? Data(contentsOf: thumbImageURL) else { return nil }
      _thumbImage = UIImage(data: imageData)
      return _thumbImage
    }
    set {
      _thumbImage = newValue
    }
  }
  
  private var _fullImage: UIImage?
  var fullImage: UIImage? {
    get {
      if _fullImage != nil { return _fullImage }
      if docPath == nil { return nil }
      
      let fullImageURL = docPath!.appendingPathComponent(Keys.fullImageFile.rawValue)
      guard let imageData = try? Data(contentsOf: fullImageURL) else { return nil }
      _fullImage = UIImage(data: imageData)
      return _fullImage
    }
    set {
      _fullImage = newValue
    }
  }
  
  var docPath: URL?
  
  init(docPath: URL) {
    super.init()
    self.docPath = docPath
  }
  
  init(title: String, rating: Float, thumbImage: UIImage?, fullImage: UIImage?) {
    super.init()
    _data = ScaryCreatureData(title: title, rating: rating)
    self.thumbImage = thumbImage
    self.fullImage = fullImage
    saveData()
    saveImages()
  }
  
  func createDataPath() throws {
    guard docPath == nil else { return }
    
    docPath = ScaryCreatureDatabase.nextScaryCreatureDocPath()
    try FileManager.default.createDirectory(at: docPath!, withIntermediateDirectories: true, attributes: nil)
  }
  
  func saveData() {
    // 1) Do nothing if there is nothing to save
    guard let data = data else { return }
    
    // 2) Create the docPath and the folder on disk
    do {
      try createDataPath()
    }catch {
      print("Couldn't create save folder. " + error.localizedDescription)
      return
    }
    
    // 3) Build the path of the file to write
    let dataURL = docPath!.appendingPathComponent(Keys.dataFile.rawValue)
    
    // 4) Encode the data using NSCoding
    let codedData = try! NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)
    
    // 5) Write the encoded data to the file.
    do {
      try codedData.write(to: dataURL)
    }catch {
      print("Couldn't write to save file: " + error.localizedDescription)
    }
  }
  
  func deleteDoc() {
    if let docPath = docPath {
      do {
        try FileManager.default.removeItem(at: docPath)
      }catch {
        print("Error Deleting Folder. " + error.localizedDescription)
      }
    }
  }
  
  func saveImages() {
    // 1) Make sure that there are images stored
    if _fullImage == nil || _thumbImage == nil { return }
    
    // 2) Create the storage folder if required
    do {
      try createDataPath()
    }catch {
      print("Couldn't create save Folder. " + error.localizedDescription)
      return
    }
    
    // 3) Build the paths for each file
    let thumbImageURL = docPath!.appendingPathComponent(Keys.thumbImageFile.rawValue)
    let fullImageURL = docPath!.appendingPathComponent(Keys.fullImageFile.rawValue)
    
    // 4) Convert the images to Data objects with a PNG representation
    let thumbImageData = _thumbImage!.pngData()
    let fullImageData = _fullImage!.pngData()
    
    // 5) Write the PNG data to disk
    try! thumbImageData!.write(to: thumbImageURL)
    try! fullImageData!.write(to: fullImageURL)
  }
}
7. ScaryCreatureDatabase.swift
import Foundation

class ScaryCreatureDatabase: NSObject {
  static let privateDocsDir: URL = {
    // 1
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    
    // 2
    let documentsDirectoryURL = paths.first!.appendingPathComponent("PrivateDocuments")
    
    // 3
    do {
      try FileManager.default.createDirectory(at: documentsDirectoryURL,
                                              withIntermediateDirectories: true,
                                              attributes: nil)
    } catch {
      print("Couldn't create directory")
    }
    return documentsDirectoryURL
  }()

  class func nextScaryCreatureDocPath() -> URL? {
    // 1) Get all the files and folders within the database folder
    guard let files = try? FileManager.default.contentsOfDirectory(at: privateDocsDir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) else { return nil }
    var maxNumber = 0
    
    // 2) Get the highest numbered item saved within the database
    files.forEach {
      if $0.pathExtension == "scarycreature" {
        let fileName = $0.deletingPathExtension().lastPathComponent
        maxNumber = max(maxNumber, Int(fileName) ?? 0)
      }
    }
    
    // 3) Return a path with the consecutive number
    return privateDocsDir.appendingPathComponent("\(maxNumber + 1).scarycreature", isDirectory: true)
  }
  
  class func loadScaryCreatureDocs() -> [ScaryCreatureDoc] {
    guard let files = try? FileManager.default.contentsOfDirectory(at: privateDocsDir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) else { return [] }
    
    return files
      .filter { $0.pathExtension == "scarycreature" }
      .map { ScaryCreatureDoc(docPath: $0) }
  }
}

后记

本篇主要讲述了基于NSCoding的持久化存储,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(数据持久化方案解析(四) —— 基于NSCoding的持久化存储(二))