版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.08.25 星期日 |
前言
数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说
plist
文件(属性列表)、preference
(偏好设置)、NSKeyedArchiver
(归档)、SQLite 3
、CoreData
,这里基本上我们都用过。这几种方案各有优缺点,其中,CoreData是苹果极力推荐我们使用的一种方式,我已经将它分离出去一个专题进行说明讲解。这个专题主要就是针对另外几种数据持久化存储方案而设立。
1. 数据持久化方案解析(一) —— 一个简单的基于SQLite持久化方案示例(一)
2. 数据持久化方案解析(二) —— 一个简单的基于SQLite持久化方案示例(二)
3. 数据持久化方案解析(三) —— 基于NSCoding的持久化存储(一)
4. 数据持久化方案解析(四) —— 基于NSCoding的持久化存储(二)
5. 数据持久化方案解析(五) —— 基于Realm的持久化存储(一)
6. 数据持久化方案解析(六) —— 基于Realm的持久化存储(二)
7. 数据持久化方案解析(七) —— 基于Realm的持久化存储(三)
8. 数据持久化方案解析(八) —— UIDocument的数据存储(一)
9. 数据持久化方案解析(九) —— UIDocument的数据存储(二)
源码
1. Swift
首先看下代码组织结构
下面看一下xib中的内容
接着就是看一下代码了
1. DetailViewController.swift
import UIKit
import Photos
import AssetsLibrary
protocol DetailViewControllerDelegate: class {
func detailViewControllerDidFinish(_ viewController: DetailViewController, with photoEntry: PhotoEntry?, title: String?)
}
class DetailViewController: UIViewController {
weak var delegate: DetailViewControllerDelegate?
var document: Document? {
didSet {
guard let doc = document else { return }
title = doc.description
}
}
@IBOutlet weak var addPhotoButton: UIButton!
@IBOutlet weak var titleTextField: UITextField!
@IBOutlet weak var fullImageView: UIImageView!
private var newImage: UIImage?
private var newThumbnailImage: UIImage?
private var hasChanges = false
override func viewDidLoad() {
super.viewDidLoad()
if PHPhotoLibrary.authorizationStatus() == .notDetermined {
PHPhotoLibrary.requestAuthorization { _ in }
}
openDocument()
}
private func showImagePicker() {
let imagePicker = UIImagePickerController()
guard UIImagePickerController.isSourceTypeAvailable(imagePicker.sourceType) else { return }
imagePicker.sourceType = .photoLibrary
imagePicker.allowsEditing = false
imagePicker.delegate = self
present(imagePicker, animated: true, completion: nil)
}
private func openDocument() {
if document == nil {
showImagePicker()
}
else {
document?.open() {
[weak self] _ in
self?.fullImageView.image = self?.document?.photo?.mainImage
self?.titleTextField.text = self?.document?.description
}
}
}
@IBAction func editPhoto(_ sender: Any) {
showImagePicker()
}
@IBAction func donePressed(_ sender: Any) {
var photoEntry: PhotoEntry?
if let newImage = newImage, let newThumb = newThumbnailImage {
photoEntry = PhotoEntry(mainImage: newImage, thumbnailImage: newThumb)
}
let hasDifferentPhoto = !newImage.isSame(photo: document?.photo?.mainImage)
let hasDifferentTitle = document?.description != titleTextField.text
hasChanges = hasDifferentPhoto || hasDifferentTitle
guard let doc = document, hasChanges else {
delegate?.detailViewControllerDidFinish(
self,
with: photoEntry,
title: titleTextField.text
)
dismiss(animated: true, completion: nil)
return
}
doc.photo = photoEntry
doc.save(to: doc.fileURL, for: .forOverwriting) { [weak self] (success) in
guard let self = self else { return }
if !success { fatalError("Failed to close doc.") }
self.delegate?.detailViewControllerDidFinish(
self,
with: photoEntry,
title: self.titleTextField.text
)
self.dismiss(animated: true, completion: nil)
}
}
@IBAction func dismiss(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
extension DetailViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
@objc func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else {
return
}
let options = PHImageRequestOptions()
options.resizeMode = .exact
options.isSynchronous = true
if let imageAsset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset {
let imageManager = PHImageManager.default()
imageManager.requestImage(
for: imageAsset,
targetSize: CGSize(width: 150, height: 150),
contentMode: .aspectFill,
options: options
) { (result, _) in
self.newThumbnailImage = result
}
}
fullImageView.image = image
let mainSize = fullImageView.bounds.size
newImage = image.imageByBestFit(for: mainSize)
picker.dismiss(animated: true, completion: nil)
}
@objc func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)
}
}
extension DetailViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
2. ViewController.swift
import UIKit
private extension String {
static let cellID = "PhotoKeeperCell"
}
class ViewController: UIViewController {
private var selectedEntry: Entry?
private var entries: [Entry] = []
private lazy var localRoot: URL? = FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask).first
private var selectedDocument: Document?
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var leftBarButtonItem: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
refresh()
}
private func loadDoc(at fileURL: URL) {
let doc = Document(fileURL: fileURL)
doc.open { [weak self] success in
guard success else {
fatalError("Failed to open doc.")
}
let metadata = doc.metadata
let fileURL = doc.fileURL
let version = NSFileVersion.currentVersionOfItem(at: fileURL)
doc.close() { success in
guard success else {
fatalError("Failed to close doc.")
}
if let version = version {
self?.addOrUpdateEntry(for: fileURL, metadata: metadata, version: version)
}
}
}
}
private func loadLocal() {
guard let root = localRoot else { return }
do {
let localDocs = try FileManager.default.contentsOfDirectory(
at: root,
includingPropertiesForKeys: nil,
options: [])
for localDoc in localDocs where localDoc.pathExtension == .appExtension {
loadDoc(at: localDoc)
}
} catch let error {
fatalError("Couldn't load local content. \(error.localizedDescription)")
}
}
private func refresh() {
loadLocal()
tableView.reloadData()
}
private func getDocumentURL(for filename: String) -> URL? {
return localRoot?.appendingPathComponent(filename, isDirectory: false)
}
private func docNameExists(for docName: String) -> Bool {
return !entries.filter{ $0.fileURL.lastPathComponent == docName }.isEmpty
}
private func indexOfEntry(for fileURL: URL) -> Int? {
return entries.firstIndex(where: { $0.fileURL == fileURL })
}
private func addOrUpdateEntry(
for fileURL: URL,
metadata: PhotoMetadata?,
version: NSFileVersion) {
if let index = indexOfEntry(for: fileURL) {
let entry = entries[index]
entry.metadata = metadata
entry.version = version
} else {
let entry = Entry(fileURL: fileURL, metadata: metadata, version: version)
entries.append(entry)
}
entries = entries.sorted(by: >)
tableView.reloadData()
}
private func insertNewDocument(
with photoEntry: PhotoEntry? = nil,
title: String? = nil) {
guard let fileURL = getDocumentURL(
for: getDocFilename(for: title ?? .photoKey)
) else { return }
let doc = Document(fileURL: fileURL)
doc.photo = photoEntry
doc.save(to: fileURL, for: .forCreating) {
[weak self] success in
guard success else {
fatalError("Failed to create file.")
}
print("File created at: \(fileURL)")
let metadata = doc.metadata
let URL = doc.fileURL
if let version = NSFileVersion.currentVersionOfItem(at: fileURL) {
self?.addOrUpdateEntry(for: URL, metadata: metadata, version: version)
}
}
}
private func showDetailVC() {
guard let detailVC = detailVC else { return }
detailVC.delegate = self
detailVC.document = selectedDocument
mode = .viewing
present(detailVC.navigationController!, animated: true, completion: nil)
}
private func getDocFilename(for prefix: String) -> String {
var newDocName = String(format: "%@.%@", prefix, String.appExtension)
var docCount = 1
while docNameExists(for: newDocName) {
newDocName = String(format: "%@ %d.%@", prefix, docCount, String.appExtension)
docCount += 1
}
return newDocName
}
private func indexOfEntry(for name: String) -> Int? {
return entries.firstIndex(where: { $0.description == name})
}
@IBAction func addEntry(_ sender: Any) {
selectedEntry = nil
selectedDocument = nil
showDetailVC()
}
@IBAction func editEntries(_ sender: Any) {
mode = mode.otherMode
}
private func delete(entry: Entry) {
let fileURL = entry.fileURL
guard let entryIndex = indexOfEntry(for: fileURL) else { return }
do {
try FileManager.default.removeItem(at: fileURL)
entries.remove(at: entryIndex)
tableView.reloadData()
} catch {
fatalError("Couldn't remove file.")
}
}
private func rename(_ entry: Entry, with name: String) {
guard entry.description != name else { return }
let newDocFilename = "\(name).\(String.appExtension)"
if docNameExists(for: newDocFilename) {
fatalError("Name already taken.")
}
guard let newDocURL = getDocumentURL(for: newDocFilename) else { return }
do {
try FileManager.default.moveItem(at: entry.fileURL, to: newDocURL)
} catch {
fatalError("Couldn't move to new URL.")
}
entry.fileURL = newDocURL
entry.version = NSFileVersion.currentVersionOfItem(at: entry.fileURL) ?? entry.version
tableView.reloadData()
}
private var mode: Mode = .viewing {
didSet {
switch mode {
case .editing:
tableView.setEditing(true, animated: true)
leftBarButtonItem.title = "Done"
case .viewing:
tableView.setEditing(false, animated: true)
leftBarButtonItem.title = "Edit"
}
}
}
}
//MARK: DetailViewControllerDelegate
extension ViewController: DetailViewControllerDelegate {
func detailViewControllerDidFinish(_ viewController: DetailViewController, with photoEntry: PhotoEntry?, title: String?) {
guard
let doc = viewController.document,
let version = NSFileVersion.currentVersionOfItem(at: doc.fileURL)
else {
if let docData = photoEntry {
insertNewDocument(with: docData, title: title)
}
return
}
if let docData = photoEntry {
doc.photo = docData
}
addOrUpdateEntry(for: doc.fileURL, metadata: doc.metadata, version: version)
if let title = title, let entry = selectedEntry, title != entry.description {
rename(entry, with: title)
}
}
}
//MARK: UITableViewDelegate
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let entry = entries[indexPath.row]
selectedEntry = entry
selectedDocument = Document(fileURL: entry.fileURL)
showDetailVC()
tableView.deselectRow(at: indexPath, animated: false)
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let entry = entries[indexPath.row]
delete(entry: entry)
}
}
}
//MARK: UITableViewDataSource
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return entries.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: .cellID, for: indexPath) as? PhotoKeeperCell else { return UITableViewCell() }
let entry = entries[indexPath.row]
cell.photoImageView?.image = entry.metadata?.image
cell.titleTextField?.text = entry.description
cell.subtitleLabel?.text = entry.version.modificationDate?.mediumString
return cell
}
}
//MARK: UITextFieldDelegate
extension ViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
mode = .viewing
guard
let entry = selectedEntry,
let newName = textField.text
else {
return true
}
rename(entry, with: newName)
return true
}
func textFieldDidBeginEditing(_ textField: UITextField) {
let filteredEntries = entries.filter { (entry) -> Bool in
return entry.description == textField.text
}
guard let entry = filteredEntries.first else { return }
selectedEntry = entry
}
func textFieldDidEndEditing(_ textField: UITextField) {
textField.resignFirstResponder()
}
}
//MARK: Additional Conveniences
extension ViewController {
private var detailVC: DetailViewController? {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let detailNavVC = storyboard.instantiateViewController(withIdentifier: "DetailNavigationController")
guard
let navVC = detailNavVC as? UINavigationController,
let detailVC = navVC.topViewController as? DetailViewController
else {
return nil
}
return detailVC
}
}
private enum Mode {
case editing
case viewing
var otherMode: Mode {
switch self {
case .editing:
return .viewing
case .viewing:
return .editing
}
}
}
3. PhotoKeeperCell.swift
import UIKit
class PhotoKeeperCell: UITableViewCell {
@IBOutlet weak var photoImageView: UIImageView!
@IBOutlet weak var titleTextField: UITextField!
@IBOutlet weak var subtitleLabel: UILabel!
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
UIView.animate(withDuration: 0.1) {
self.titleTextField.isEnabled = editing
self.titleTextField.borderStyle = editing ? UITextField.BorderStyle.roundedRect : .none
}
}
}
4. Date+Extensions.swift
import Foundation
private var mediumFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.doesRelativeDateFormatting = true
dateFormatter.timeStyle = .medium
dateFormatter.dateStyle = .medium
return dateFormatter
}()
extension Date {
var mediumString: String {
return mediumFormatter.string(from: self)
}
}
5. UIImage+Extensions.swift
import UIKit
extension UIImage {
static var `default`: UIImage {
return #imageLiteral(resourceName: "default")
}
func imageWith(newSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: newSize)
let image = renderer.image {_ in
draw(in: CGRect(origin: .zero, size: newSize))
}
return image
}
func imageByBestFit(for targetSize: CGSize) -> UIImage {
let aspectRatio = size.width / size.height
let targetHeight = targetSize.height
let scaledWidth = targetSize.height * aspectRatio
let bestSize = CGSize(width: scaledWidth, height: targetHeight)
return imageWith(newSize: bestSize)
}
}
extension Optional where Wrapped == UIImage {
func isSame(photo: UIImage?) -> Bool {
switch (self, photo) {
case (nil, nil):
return true
case (nil, _), (_, nil):
return false
case (let p1, let p2):
return p1!.isEqual(p2)
}
}
}
6. PhotoData.swift
import Foundation
import UIKit
class PhotoData: NSObject, NSCoding {
var image: UIImage?
init(image: UIImage? = nil) {
self.image = image
}
func encode(with aCoder: NSCoder) {
aCoder.encode(1, forKey: .versionKey)
guard let photoData = image?.pngData() else { return }
aCoder.encode(photoData, forKey: .photoKey)
}
required init?(coder aDecoder: NSCoder) {
aDecoder.decodeInteger(forKey: .versionKey)
guard let photoData = aDecoder.decodeObject(forKey: .photoKey) as? Data else { return nil }
self.image = UIImage(data: photoData)
}
}
7. PhotoMetadata.swift
import Foundation
import UIKit
class PhotoMetadata: NSObject, NSCoding {
var image: UIImage?
init(image: UIImage? = nil) {
self.image = image
}
func encode(with aCoder: NSCoder) {
aCoder.encode(1, forKey: .versionKey)
guard let photoData = image?.pngData() else { return }
aCoder.encode(photoData, forKey: .thumbnailKey)
}
required init?(coder aDecoder: NSCoder) {
aDecoder.decodeInteger(forKey: .versionKey)
guard let photoData = aDecoder.decodeObject(forKey: .thumbnailKey) as? Data else { return }
image = UIImage(data: photoData)
}
}
8. Entry.swift
import Foundation
import UIKit
class Entry: NSObject {
var fileURL: URL
var metadata: PhotoMetadata?
var version: NSFileVersion
private var editDate: Date {
return version.modificationDate ?? .distantPast
}
override var description: String {
return fileURL.deletingPathExtension().lastPathComponent
}
init(fileURL: URL, metadata: PhotoMetadata?, version: NSFileVersion) {
self.fileURL = fileURL
self.metadata = metadata
self.version = version
}
}
extension Entry: Comparable {
static func < (lhs: Entry, rhs: Entry) -> Bool {
return lhs.editDate < rhs.editDate
}
}
9. Document.swift
import UIKit
extension String {
static let appExtension: String = "ptk"
static let versionKey: String = "Version"
static let photoKey: String = "Photo"
static let thumbnailKey: String = "Thumbnail"
}
typealias PhotoEntry = (mainImage: UIImage?, thumbnailImage: UIImage?)
private extension String {
static let dataKey: String = "Data"
static let metadataFilename: String = "photo.metadata"
static let dataFilename: String = "photo.data"
}
class Document: UIDocument {
override var description: String {
return fileURL.deletingPathExtension().lastPathComponent
}
var fileWrapper: FileWrapper?
lazy var photoData: PhotoData = {
guard
fileWrapper != nil,
let data = decodeFromWrapper(for: .dataFilename) as? PhotoData
else {
return PhotoData()
}
return data
}()
lazy var metadata: PhotoMetadata = {
guard
fileWrapper != nil,
let data = decodeFromWrapper(for: .metadataFilename) as? PhotoMetadata
else {
return PhotoMetadata()
}
return data
}()
// 4
var photo: PhotoEntry? {
get {
return PhotoEntry(mainImage: photoData.image, thumbnailImage: metadata.image)
}
set {
photoData.image = newValue?.mainImage
metadata.image = newValue?.thumbnailImage
}
}
private func encodeToWrapper(object: NSCoding) -> FileWrapper {
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
archiver.encode(object, forKey: .dataKey)
archiver.finishEncoding()
return FileWrapper(regularFileWithContents: archiver.encodedData)
}
override func contents(forType typeName: String) throws -> Any {
let metaDataWrapper = encodeToWrapper(object: metadata)
let photoDataWrapper = encodeToWrapper(object: photoData)
let wrappers: [String: FileWrapper] = [.metadataFilename: metaDataWrapper,
.dataFilename: photoDataWrapper]
return FileWrapper(directoryWithFileWrappers: wrappers)
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let contents = contents as? FileWrapper else { return }
fileWrapper = contents
}
func decodeFromWrapper(for name: String) -> Any? {
guard let allWrappers = fileWrapper,
let wrapper = allWrappers.fileWrappers?[name],
let data = wrapper.regularFileContents else { return nil }
do {
let unarchiver = try NSKeyedUnarchiver.init(forReadingFrom: data)
unarchiver.requiresSecureCoding = false
return unarchiver.decodeObject(forKey: .dataKey)
} catch let error {
fatalError("Unarchiving failed. \(error.localizedDescription)")
}
}
}
后记
本篇主要讲述了UIDocument的数据存储,感兴趣的给个赞或者关注~~~