数据持久化方案解析(四) —— 基于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..<maxRating {
      let imageView = UIImageView()
      imageView.contentMode = .scaleAspectFill
      imageViews.append(imageView)
      addSubview(imageView)
    }
    
    setNeedsLayout()
    refresh()
  }
  
  func handleTouch(touchLocation: CGPoint) {
    guard editable else { return }
    
    var newRating = 0
    
    for i in stride(from: imageViews.count - 1, to: -1, by: -1) {
      let imageView = imageViews[i]
      if touchLocation.x > imageView.frame.minX {
        newRating = i + 1
        break;
      }
    }
    
    rating = Float(newRating)
  }
  
  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    if let touch = touches.first {
      let touchLocation = touch.location(in: self)
      handleTouch(touchLocation: touchLocation)
    }
  }
  
  override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    if let touch = touches.first {
      let touchLocation = touch.location(in: self)
      handleTouch(touchLocation: touchLocation)
    }
  }
  
  override func touchesEnded(_ touches: Set<UITouch>, 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的持久化存储,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容