版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.10.26 星期五 |
前言
数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说
plist
文件(属性列表)、preference
(偏好设置)、NSKeyedArchiver
(归档)、SQLite 3
、CoreData
,这里基本上我们都用过。这几种方案各有优缺点,其中,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的持久化存储,感兴趣的给个赞或者关注~~~