数据持久化方案解析(九) —— UIDocument的数据存储(二)

版本记录

版本号 时间
V1.0 2019.08.25 星期日

前言

数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说plist文件(属性列表)、preference(偏好设置)、NSKeyedArchiver(归档)、SQLite 3CoreData,这里基本上我们都用过。这几种方案各有优缺点,其中,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添加的UIDocumentBrowserViewControllerFiles应用程序通过提供对应用程序中管理文件的轻松访问,使生活变得更加简单。 但是如果你想要更细粒度的控制呢?

在本教程中,您将学习如何在iOS文件系统中从头开始创建,检索,编辑和删除UIDocument。 这包括四个主题:

  • 1) 创建数据模型。
  • 2) 子类化UIDocument
  • 3) 创建和列出UIDocument
  • 4) 更新和删除UIDocument

注意:本教程假设您已经熟悉NSCoding,协议和代理模式和Swift中的错误处理。

在本教程中,您将创建一个名为PhotoKeeper的应用程序,它允许您存储和命名您喜欢的照片。

打开入门项目。 然后,构建并运行。

您可以通过点击右侧的+按钮向table view添加条目,然后点击左侧的Edit按钮进行编辑。

您最终使用的应用程序将允许您选择并命名您喜欢的照片。 您还可以更改照片或标题或完全删除它。


Data Models

UIDocument支持两个不同的输入/输出类:

  • Data:一个简单的数据缓冲区。当您的文档是单个文件时使用此选项。
  • FileWrapperOS视为单个文件的文件包目录。当您的文档包含要独立加载的多个文件时,这非常棒。

本教程的数据模型非常简单:它只是一张照片!因此,使用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”NSCodingkey

现在打开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)
  }
}

PhotoMetadataPhotoData相同,只是它存储的图像要小得多。 在功能更全面的应用程序中,您可以在此处存储有关照片的其他信息(如注释或评级),这就是为什么它是一个单独的类型。

恭喜,您现在拥有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) fileWrapperOS文件系统节点,表示包含照片和元数据的目录。
  • 3) photoDataphotoMetadata是用于解释fileWrapper包含的photo.metadataphoto.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协议将数据内容转换回对象。

最后要做的是为photoDataphotoMetadata实现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)
}

在这里,您获取用户选择的条目,填充selectedEntryselectedDocument属性并显示详细视图控制器。

现在将addEntry(_ :)实现替换为:

selectedEntry = nil
selectedDocument = nil
showDetailVC()

在此处清空selectedEntryselectedDocument,然后显示详细视图控制器以指示您要创建新文档。

建立并运行。 现在尝试添加一个新条目。

看起来不错,但是点击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,创建缩略图并将完整图像和缩略图图像保存在各自的局部变量newImagenewThumbnailImage中。

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.")
}

要删除,请使用FileManagerremoveItem(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()

对于重命名,使用FileManagermoveItem(at:to :)方法。 上述方法中的其他所有内容都是您的普通表视图管理。 很简单,嗯?

最后要做的是检查用户是否在detailViewControllerDidFinish(_:with:title :)中更改了文档的标题。

返回到该委托方法并在最后添加此代码:

if let title = title, let entry = selectedEntry, title != entry.description {
  rename(entry, with: title)
}

最后,构建并运行以尝试这种存储照片的真棒新方法!

如果您有兴趣深入创建自己的文档和管理文件,请查看Apple有关UIDcocumentFileManager的文档

后记

本篇主要讲述了UIDocument的数据存储,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容