版本记录
版本号 | 时间 |
---|---|
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的数据存储(一)
开始
首先看下主要内容
了解如何使用
UIDocument
向您的应用添加文档支持。
下面看一下写作环境
Swift 5, iOS 13, Xcode 11
有几种方法可以在iOS系统中存储数据:
- 1)
UserDefaults
用于少量数据。 - 2)
Core Data
用于大量数据。 - 3) 当您的应用程序基于用户可以创建,读取,更新和删除的单个文档的概念时用
UIDocuments
。
iOS 11
添加的UIDocumentBrowserViewController
和Files
应用程序通过提供对应用程序中管理文件的轻松访问,使生活变得更加简单。 但是如果你想要更细粒度的控制呢?
在本教程中,您将学习如何在iOS文件系统中从头开始创建,检索,编辑和删除UIDocument
。 这包括四个主题:
- 1) 创建数据模型。
- 2) 子类化
UIDocument
。 - 3) 创建和列出
UIDocument
。 - 4) 更新和删除
UIDocument
。
注意:本教程假设您已经熟悉NSCoding,协议和代理模式和Swift中的错误处理。
在本教程中,您将创建一个名为PhotoKeeper
的应用程序,它允许您存储和命名您喜欢的照片。
打开入门项目。 然后,构建并运行。
您可以通过点击右侧的+
按钮向table view
添加条目,然后点击左侧的Edit
按钮进行编辑。
您最终使用的应用程序将允许您选择并命名您喜欢的照片。 您还可以更改照片或标题或完全删除它。
Data Models
UIDocument
支持两个不同的输入/输出类:
- Data:一个简单的数据缓冲区。当您的文档是单个文件时使用此选项。
- FileWrapper:
OS
视为单个文件的文件包目录。当您的文档包含要独立加载的多个文件时,这非常棒。
本教程的数据模型非常简单:它只是一张照片!因此,使用Data似乎最有意义。
但是,您希望在用户打开文件之前在主视图控制器中显示照片的缩略图。如果您使用了Data,则必须打开并解码磁盘中的每个文档以获取缩略图。由于图像可能非常大,这可能导致性能降低和高内存开销。
所以,你将使用FileWrapper。您将在包装器中存储两个文档:
- 1)
PhotoData
代表全尺寸照片。 - 2)
PhotoMetadata
表示照片缩略图。这是应用程序可以快速加载的少量数据。
首先,定义一些常量。打开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"
}
记住:
-
“ptk”
是您应用的特定文件扩展名,因此您可以将该目录标识为您的应用知道如何处理的文档。 -
“Version”
是编码和解码文件版本号的key
,因此如果您希望将来支持旧文件,则可以更新数据结构。 -
“Photo”
和“Thumbnail”
是NSCoding
的key
。
现在打开PhotoData.swift
并实现PhotoData
类:
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)
}
}
PhotoData
是一个简单的NSObject
,它包含完整大小的图像和自己的版本号。 您实现NSCoding
协议以对这些协议进行编码和解码到数据缓冲区。
接下来,打开PhotoMetadata.swift
并在imports
后粘贴它:
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 nil
}
image = UIImage(data: photoData)
}
}
PhotoMetadata
与PhotoData
相同,只是它存储的图像要小得多。 在功能更全面的应用程序中,您可以在此处存储有关照片的其他信息(如注释或评级),这就是为什么它是一个单独的类型。
恭喜,您现在拥有PhotoKeeper
的模型类!
Subclassing UIDocument
UIDocument
是一个抽象基类。 这意味着您必须将其子类化并实现某些必需的方法才能使用它们。 特别是,您必须重写两个方法:
-
load(fromContents:ofType :)
这是您读取document
并解码模型数据的地方。 -
contents(forType :)
使用此命令将模型写入文档document
。
首先,您将定义更多常量。 打开Document.swift
,然后将其添加到Document
的类定义上方:
private extension String {
static let dataKey: String = "Data"
static let metadataFilename: String = "photo.metadata"
static let dataFilename: String = "photo.data"
}
您将使用这些常量来编码和解码您的UIDocument
文件。
接下来,将这些属性添加到Document
类:
// 1
override var description: String {
return fileURL.deletingPathExtension().lastPathComponent
}
// 2
var fileWrapper: FileWrapper?
// 3
lazy var photoData: PhotoData = {
// TODO: Implement initializer
return PhotoData()
}()
lazy var metadata: PhotoMetadata = {
// TODO: Implement initializer
return PhotoMetadata()
}()
// 4
var photo: PhotoEntry? {
get {
return PhotoEntry(mainImage: photoData.image, thumbnailImage: metadata.image)
}
set {
photoData.image = newValue?.mainImage
metadata.image = newValue?.thumbnailImage
}
}
这是你做的:
- 1) 您通过获取
fileURL
,删除“ptk”
扩展并抓取路径组件的最后一部分来重写description
以返回文档的标题。 - 2)
fileWrapper
是OS
文件系统节点,表示包含照片和元数据的目录。 - 3)
photoData
和photoMetadata
是用于解释fileWrapper
包含的photo.metadata
和photo.data
子文件的数据模型。 这些是惰性变量,您将添加代码以便稍后从文件中提取它们。 - 4)
photo
是用于在进行更改时访问和更新主图像和缩略图图像的属性。 它的别名PhotoEntry
类型包含您的两个图像。
接下来,是时候添加代码以将UIDocument
写入磁盘。
首先,在刚刚添加的属性下面添加这些方法:
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)
}
encodeToWrapper(object :)
使用NSKeyedArchiver
将实现NSCoding
的对象转换为数据缓冲区。 然后,它使用缓冲区创建一个FileWrapper
文件,并将其添加到目录中。
要将数据写入文档,请实现contents(forType:)
。 您将每个模型类型编码为FileWrapper
,然后创建一个包含文件名作为key
的包装器字典。 最后,使用此字典创建另一个包装目录的FileWrapper
。
很好! 现在你可以实现阅读了。 添加以下方法:
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)")
}
}
您需要load(fromContents:ofType:)
来实现读取。 您所做的只是使用内容初始化fileWrapper
。
decodeFromWrapper(for :)
与encodeToWrapper(object :)
相反。 它从FileWrapper
目录中读取相应的FileWrapper
文件,并通过NSCoding
协议将数据内容转换回对象。
最后要做的是为photoData
和photoMetadata
实现getter
。
首先,将photoData
的延迟初始化程序替换为:
//1
guard
fileWrapper != nil,
let data = decodeFromWrapper(for: .dataFilename) as? PhotoData
else {
return PhotoData()
}
return data
然后,将photoMetadata
的延迟初始化程序替换为:
guard
fileWrapper != nil,
let data = decodeFromWrapper(for: .metadataFilename) as? PhotoMetadata
else {
return PhotoMetadata()
}
return data
两个惰性初始化器都做了几乎相同的事情,但是它们寻找具有不同名称的fileWrappers
。 您尝试将fileWrapper
目录中的相应文件解码为数据模型类的实例。
Creating Documents
在显示文档列表之前,您需要至少添加一个文档才能查看。 在此应用中创建新文档需要做三件事:
- 1) 存储条目。
- 2) 查找可用的URL。
- 3) 创建文档
document
。
1. Storing Entries
如果您在应用程序中创建条目,您将在单元格中看到创建日期。 您希望显示有关文档的信息,例如缩略图或您自己的文本,而不是显示日期。
所有这些信息都保存在另一个名为Entry
的类中。 每个Entry
由表视图中的单元格表示。
首先,打开Entry.swift
并替换类实现 - 但不是Comparable
扩展! - 用:
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
}
}
Entry
只是跟踪上面讨论的所有项目。 确保你没有删除Comparable
!
此时,您将看到编译器错误,因此您必须稍微清理代码。
现在,转到ViewController.swift
并删除这段代码。 你稍后会替换它:
private func addOrUpdateEntry() {
let entry = Entry()
entries.append(entry)
tableView.reloadData()
}
由于您刚刚删除了addOrUpdateEntry
,因此您将看到另一个编译器错误:
删除addEntry(_ :)
中调用addOrUpdateEntry()
的行。
2. Finding an Available URL
下一步是找到要在其中创建文档的URL
。 这并不像听起来那么容易,因为你需要自动生成一个尚未采用的文件名。 首先,您将检查文件是否存在。
转到ViewController.swift
。 在顶部,您将看到两个属性:
private var selectedEntry: Entry?
private var entries: [Entry] = []
-
selectedEntry
将帮助您跟踪用户正在与之交互的条目。 -
entries
是一个包含磁盘上所有条目的数组。
要检查文件是否存在,请查看entries
以查看是否已使用该名称。
现在,再添加两个属性:
private lazy var localRoot: URL? = FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask).first
private var selectedDocument: Document?
localRoot
实例变量跟踪文档的目录。 selectedDocument
将用于在主视图控制器和详细视图控制器之间传递数据。
现在,在viewDidLoad()
下添加此方法以返回特定文件名的文件的完整路径:
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 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
}
getDocFilename(for :)
以传入的文档名称开头,并检查它是否可用。 如果没有,它会在名称末尾添加1
并再次尝试,直到找到可用名称。
3. Creating a Document
创建Document
有两个步骤。 首先,使用URL
初始化Document
以将文件保存到。 然后,调用saveToURL
以保存文件。
创建文档后,需要更新对象数组以存储文档并显示详细视图控制器。
现在在indexOfEntry(for :)
下面添加此代码,以查找特定fileURL
的条目索引:
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()
}
addOrUpdateEntry(for:metadata:version :)
查找特定fileURL
的条目索引。 如果存在,则更新其属性。 如果没有,则创建一个新Entry
。
最后,添加一个插入新文档的方法:
private func insertNewDocument(
with photoEntry: PhotoEntry? = nil,
title: String? = nil) {
// 1
guard let fileURL = getDocumentURL(
for: getDocFilename(for: title ?? .photoKey)
) else { return }
// 2
let doc = Document(fileURL: fileURL)
doc.photo = photoEntry
// 3
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) {
// 4
self?.addOrUpdateEntry(for: URL, metadata: metadata, version: version)
}
}
}
你终于把你写的所有帮助方法都用得很好了。 在这里,您添加的代码:
- 1) 在本地目录中查找可用的文件URL。
- 2) 初始化文档
Document
。 - 3) 立即保存文档。
- 4) 向表中添加条目。
现在,将以下内容添加到addEntry(_ :)
以调用您的新代码:
insertNewDocument()
4. Final Changes
你几乎准备好测试一下了!
找到tableView(_:cellForRowAt :)
并将单元格配置替换为:
cell.photoImageView?.image = entry.metadata?.image
cell.titleTextField?.text = entry.description
cell.subtitleLabel?.text = entry.version.modificationDate?.mediumString
构建并运行您的项目。 您现在应该可以点击+
按钮来创建存储在文件系统中的新文档documents
:
如果查看控制台输出,您应该看到显示保存文档的完整路径的消息,如下所示:
File created at: file:///Users/leamaroltsonnenschein/Library/Developer/CoreSimulator/Devices/C1176DC2-9AF9-48AB-A488-A1AB76EEE8E7/data/Containers/Data/Application/B9D5780E-28CA-4CE9-A823-0808F8091E02/Documents/Photo.PTK
但是,这个应用程序有一个大问题。 如果您再次构建并运行该应用程序,列表中不会显示任何内容!
那是因为还没有列出文件的代码。 你现在就加上。
Listing Local Documents
要列出本地文档,您将获取本地Documents
目录中所有文档的URL并打开每个文档。 您将读取元数据以获取缩略图而不是数据,因此保持高效。 然后,您将再次关闭它并将其添加到表视图中。
在ViewController.swift
中,您需要添加在给定文件URL的情况下加载文档的方法。 在viewDidLoad()
下面添加此权限:
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)
}
}
}
}
在这里打开文档,获取创建条目所需的信息并显示缩略图。 然后再将其关闭而不是保持打开状态。 这有两个重要原因:
- 1) 当您只需要一个部件时,它可以避免将整个
UIDocument
保留在内存中的开销。 - 2)
UIDocuments
只能打开和关闭一次。 如果要再次打开相同的fileURL
,则必须创建新的UIDocument
实例。
添加这些方法以在刚刚添加的方法下执行刷新:
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()
}
此代码遍历Documents
目录中的所有文件,并使用应用程序的文件扩展名加载每个文档。
现在,您需要将以下内容添加到viewDidLoad()
的底部,以便在应用启动时加载文档列表:
refresh()
建立并运行。 现在,您的应用程序应该正确地选择自上次运行以来的文档列表。
Creating Actual Entries
现在是时候为PhotoKeeper
创建真正的条目了。 添加照片有两种情况:
- 1) 添加新条目。
- 2) 编辑旧条目。
这两种情况都将呈现DetailViewController
。 但是,当用户想要编辑条目时,您将把该文档从ViewController
上的selectedDocument
属性传递到DetailViewController
上的document
属性。
仍然在ViewController.swift
中,添加一个方法,在insertNewDocument(with:title:)
下面显示详细视图控制器:
private func showDetailVC() {
guard let detailVC = detailVC else { return }
detailVC.delegate = self
detailVC.document = selectedDocument
mode = .viewing
present(detailVC.navigationController!, animated: true, completion: nil)
}
如果可能,在这里访问计算属性detailVC
,并传递selectedDocument
(如果存在)。 如果它是nil
,那么你知道你正在创建一个新文档。 mode = .viewing
让视图控制器知道它正在查看而不是编辑模式。
现在转到UITableViewDelegate
扩展并实现tableView(_:didSelectRowAt)
:
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)
}
在这里,您获取用户选择的条目,填充selectedEntry
和selectedDocument
属性并显示详细视图控制器。
现在将addEntry(_ :)
实现替换为:
selectedEntry = nil
selectedDocument = nil
showDetailVC()
在此处清空selectedEntry
和selectedDocument
,然后显示详细视图控制器以指示您要创建新文档。
建立并运行。 现在尝试添加一个新条目。
看起来不错,但是点击Done
时没有任何反应。 是时候解决了!
条目由标题和两个图像组成。 用户可以在文本字段中键入标题,并在点击Add/Edit Photo
按钮后通过与UIImagePickerController
交互来选择照片。
转到DetailViewController.swift
。
首先,您需要实现openDocument()
。 它在viewDidLoad()
的末尾被调用,以最终打开文档并访问完整大小的图像。 将此代码添加到openDocument()
:
if document == nil {
showImagePicker()
}
else {
document?.open() { [weak self] _ in
self?.fullImageView.image = self?.document?.photo?.mainImage
self?.titleTextField.text = self?.document?.description
}
}
打开文档后,将存储的图像分配给fullImageView
,将文档的description
分配为标题。
Store and Crop
当用户选择他们的图像时,UIImagePickerController
返回imagePickerController(_:didFinishPickingMediaWithInfo:)
中的信息。
此时,您希望将所选图像分配给fullImageView
,创建缩略图并将完整图像和缩略图图像保存在各自的局部变量newImage
和newThumbnailImage
中。
将imagePickerController(_:didFinishPickingMediaWithInfo :)
中的代码替换为:
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)
确保用户选择图像后,使用Photos and AssetsLibrary
框架创建缩略图。 而不是必须弄清楚要裁剪的图像最相关的矩形是你自己,这两个框架为你做了!
事实上,缩略图看起来与Photos
库中的缩略图完全相同:
Compare and Save
最后,您需要实现用户点击Done
按钮时发生的情况。
所以,用以下内容更新donePressed(_ :)
:
var photoEntry: PhotoEntry?
if let newImage = newImage, let newThumb = newThumbnailImage {
photoEntry = PhotoEntry(mainImage: newImage, thumbnailImage: newThumb)
}
// 1
let hasDifferentPhoto = !newImage.isSame(photo: document?.photo?.mainImage)
let hasDifferentTitle = document?.description != titleTextField.text
hasChanges = hasDifferentPhoto || hasDifferentTitle
// 2
guard let doc = document, hasChanges else {
delegate?.detailViewControllerDidFinish(
self,
with: photoEntry,
title: titleTextField.text
)
dismiss(animated: true, completion: nil)
return
}
// 3
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)
}
确保存在适当的图像后:
- 1) 通过将新图像与文档进行比较,检查图像或标题是否有变化。
- 2) 如果未传递现有文档,则将控制权交给代理(主视图控制器)。
- 3) 如果您确实传递了一个文档,那么首先保存并覆盖它,然后让代理发挥其魔力。
Insert or Update
最后一个难题是在主视图控制器上插入或更新这些新数据。
转到ViewController.swift
并找到DetailViewControllerDelegate
扩展并实现空委托方法detailViewControllerDidFinish(_:with:title :)
:
// 1
guard
let doc = viewController.document,
let version = NSFileVersion.currentVersionOfItem(at: doc.fileURL)
else {
if let docData = photoEntry {
insertNewDocument(with: docData, title: title)
}
return
}
// 2
if let docData = photoEntry {
doc.photo = docData
}
addOrUpdateEntry(for: doc.fileURL, metadata: doc.metadata, version: version)
这是你添加的内容:
- 1) 如果详细视图控制器没有文档,则插入一个新文档。
- 2) 如果文档存在,则只需更新旧条目。
现在,构建并运行以查看此操作:
成功! 您最终可以创建正确的条目甚至编辑照片! 但是,如果您尝试更改标题或删除条目,则更改将只是暂时的,并在您退出并打开应用程序时返回。
Deleting and Renaming
对于删除和重命名文档,您将使用FileManager
,它允许您访问共享文件管理器对象,该对象允许您与文件系统的内容进行交互并对其进行更改。
首先,返回ViewController.swift
并将delete(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.")
}
要删除,请使用FileManager
的removeItem(at :)
方法。 在构建和运行时,您会看到现在可以滑动行以永久删除它们。 请务必关闭并重新启动应用以验证它们是否已经消失。
接下来,您将添加重命名文档的功能。
首先,添加以下代码到rename(_:with:)
:
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()
对于重命名,使用FileManager
的moveItem(at:to :)
方法。 上述方法中的其他所有内容都是您的普通表视图管理。 很简单,嗯?
最后要做的是检查用户是否在detailViewControllerDidFinish(_:with:title :)
中更改了文档的标题。
返回到该委托方法并在最后添加此代码:
if let title = title, let entry = selectedEntry, title != entry.description {
rename(entry, with: title)
}
最后,构建并运行以尝试这种存储照片的真棒新方法!
如果您有兴趣深入创建自己的文档和管理文件,请查看Apple有关UIDcocument和FileManager的文档
后记
本篇主要讲述了UIDocument的数据存储,感兴趣的给个赞或者关注~~~