數據持久化方案解析(九) —— 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的數據存儲,感興趣的給個贊或者關注~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容