數(shù)據(jù)持久化方案解析(九) —— UIDocument的數(shù)據(jù)存儲(chǔ)(二)

版本記錄

版本號(hào) 時(shí)間
V1.0 2019.08.25 星期日

前言

數(shù)據(jù)的持久化存儲(chǔ)是移動(dòng)端不可避免的一個(gè)問題,很多時(shí)候的業(yè)務(wù)邏輯都需要我們進(jìn)行本地化存儲(chǔ)解決和完成,我們可以采用很多持久化存儲(chǔ)方案,比如說plist文件(屬性列表)、preference(偏好設(shè)置)、NSKeyedArchiver(歸檔)、SQLite 3、CoreData,這里基本上我們都用過。這幾種方案各有優(yōu)缺點(diǎn),其中,CoreData是蘋果極力推薦我們使用的一種方式,我已經(jīng)將它分離出去一個(gè)專題進(jìn)行說明講解。這個(gè)專題主要就是針對(duì)另外幾種數(shù)據(jù)持久化存儲(chǔ)方案而設(shè)立。
1. 數(shù)據(jù)持久化方案解析(一) —— 一個(gè)簡單的基于SQLite持久化方案示例(一)
2. 數(shù)據(jù)持久化方案解析(二) —— 一個(gè)簡單的基于SQLite持久化方案示例(二)
3. 數(shù)據(jù)持久化方案解析(三) —— 基于NSCoding的持久化存儲(chǔ)(一)
4. 數(shù)據(jù)持久化方案解析(四) —— 基于NSCoding的持久化存儲(chǔ)(二)
5. 數(shù)據(jù)持久化方案解析(五) —— 基于Realm的持久化存儲(chǔ)(一)
6. 數(shù)據(jù)持久化方案解析(六) —— 基于Realm的持久化存儲(chǔ)(二)
7. 數(shù)據(jù)持久化方案解析(七) —— 基于Realm的持久化存儲(chǔ)(三)
8. 數(shù)據(jù)持久化方案解析(八) —— UIDocument的數(shù)據(jù)存儲(chǔ)(一)

開始

首先看下主要內(nèi)容

了解如何使用UIDocument向您的應(yīng)用添加文檔支持。

下面看一下寫作環(huán)境

Swift 5, iOS 13, Xcode 11

有幾種方法可以在iOS系統(tǒng)中存儲(chǔ)數(shù)據(jù):

  • 1) UserDefaults用于少量數(shù)據(jù)。
  • 2) Core Data用于大量數(shù)據(jù)。
  • 3) 當(dāng)您的應(yīng)用程序基于用戶可以創(chuàng)建,讀取,更新和刪除的單個(gè)文檔的概念時(shí)用UIDocuments

iOS 11添加的UIDocumentBrowserViewControllerFiles應(yīng)用程序通過提供對(duì)應(yīng)用程序中管理文件的輕松訪問,使生活變得更加簡單。 但是如果你想要更細(xì)粒度的控制呢?

在本教程中,您將學(xué)習(xí)如何在iOS文件系統(tǒng)中從頭開始創(chuàng)建,檢索,編輯和刪除UIDocument。 這包括四個(gè)主題:

  • 1) 創(chuàng)建數(shù)據(jù)模型。
  • 2) 子類化UIDocument。
  • 3) 創(chuàng)建和列出UIDocument。
  • 4) 更新和刪除UIDocument。

注意:本教程假設(shè)您已經(jīng)熟悉NSCoding,協(xié)議和代理模式和Swift中的錯(cuò)誤處理。

在本教程中,您將創(chuàng)建一個(gè)名為PhotoKeeper的應(yīng)用程序,它允許您存儲(chǔ)和命名您喜歡的照片。

打開入門項(xiàng)目。 然后,構(gòu)建并運(yùn)行。

您可以通過點(diǎn)擊右側(cè)的+按鈕向table view添加條目,然后點(diǎn)擊左側(cè)的Edit按鈕進(jìn)行編輯。

您最終使用的應(yīng)用程序?qū)⒃试S您選擇并命名您喜歡的照片。 您還可以更改照片或標(biāo)題或完全刪除它。


Data Models

UIDocument支持兩個(gè)不同的輸入/輸出類:

  • Data:一個(gè)簡單的數(shù)據(jù)緩沖區(qū)。當(dāng)您的文檔是單個(gè)文件時(shí)使用此選項(xiàng)。
  • FileWrapperOS視為單個(gè)文件的文件包目錄。當(dāng)您的文檔包含要獨(dú)立加載的多個(gè)文件時(shí),這非常棒。

本教程的數(shù)據(jù)模型非常簡單:它只是一張照片!因此,使用Data似乎最有意義。

但是,您希望在用戶打開文件之前在主視圖控制器中顯示照片的縮略圖。如果您使用了Data,則必須打開并解碼磁盤中的每個(gè)文檔以獲取縮略圖。由于圖像可能非常大,這可能導(dǎo)致性能降低和高內(nèi)存開銷。

所以,你將使用FileWrapper。您將在包裝器中存儲(chǔ)兩個(gè)文檔:

  • 1) PhotoData代表全尺寸照片。
  • 2) PhotoMetadata表示照片縮略圖。這是應(yīng)用程序可以快速加載的少量數(shù)據(jù)。

首先,定義一些常量。打開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"
}

記?。?/p>

  • “ptk”是您應(yīng)用的特定文件擴(kuò)展名,因此您可以將該目錄標(biāo)識(shí)為您的應(yīng)用知道如何處理的文檔。
  • “Version”是編碼和解碼文件版本號(hào)的key,因此如果您希望將來支持舊文件,則可以更新數(shù)據(jù)結(jié)構(gòu)。
  • “Photo”“Thumbnail”NSCodingkey。

現(xiàn)在打開PhotoData.swift并實(shí)現(xiàn)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是一個(gè)簡單的NSObject,它包含完整大小的圖像和自己的版本號(hào)。 您實(shí)現(xiàn)NSCoding協(xié)議以對(duì)這些協(xié)議進(jìn)行編碼和解碼到數(shù)據(jù)緩沖區(qū)。

接下來,打開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相同,只是它存儲(chǔ)的圖像要小得多。 在功能更全面的應(yīng)用程序中,您可以在此處存儲(chǔ)有關(guān)照片的其他信息(如注釋或評(píng)級(jí)),這就是為什么它是一個(gè)單獨(dú)的類型。

恭喜,您現(xiàn)在擁有PhotoKeeper的模型類!


Subclassing UIDocument

UIDocument是一個(gè)抽象基類。 這意味著您必須將其子類化并實(shí)現(xiàn)某些必需的方法才能使用它們。 特別是,您必須重寫兩個(gè)方法:

  • load(fromContents:ofType :)這是您讀取document并解碼模型數(shù)據(jù)的地方。
  • 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”擴(kuò)展并抓取路徑組件的最后一部分來重寫description以返回文檔的標(biāo)題。
  • 2) fileWrapperOS文件系統(tǒng)節(jié)點(diǎn),表示包含照片和元數(shù)據(jù)的目錄。
  • 3) photoDataphotoMetadata是用于解釋fileWrapper包含的photo.metadataphoto.data子文件的數(shù)據(jù)模型。 這些是惰性變量,您將添加代碼以便稍后從文件中提取它們。
  • 4) photo是用于在進(jìn)行更改時(shí)訪問和更新主圖像和縮略圖圖像的屬性。 它的別名PhotoEntry類型包含您的兩個(gè)圖像。

接下來,是時(shí)候添加代碼以將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將實(shí)現(xiàn)NSCoding的對(duì)象轉(zhuǎn)換為數(shù)據(jù)緩沖區(qū)。 然后,它使用緩沖區(qū)創(chuàng)建一個(gè)FileWrapper文件,并將其添加到目錄中。

要將數(shù)據(jù)寫入文檔,請實(shí)現(xiàn)contents(forType:)。 您將每個(gè)模型類型編碼為FileWrapper,然后創(chuàng)建一個(gè)包含文件名作為key的包裝器字典。 最后,使用此字典創(chuàng)建另一個(gè)包裝目錄的FileWrapper。

很好! 現(xiàn)在你可以實(shí)現(xiàn)閱讀了。 添加以下方法:

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:)來實(shí)現(xiàn)讀取。 您所做的只是使用內(nèi)容初始化fileWrapper。

decodeFromWrapper(for :)encodeToWrapper(object :)相反。 它從FileWrapper目錄中讀取相應(yīng)的FileWrapper文件,并通過NSCoding協(xié)議將數(shù)據(jù)內(nèi)容轉(zhuǎn)換回對(duì)象。

最后要做的是為photoDataphotoMetadata實(shí)現(xiàn)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

兩個(gè)惰性初始化器都做了幾乎相同的事情,但是它們尋找具有不同名稱的fileWrappers。 您嘗試將fileWrapper目錄中的相應(yīng)文件解碼為數(shù)據(jù)模型類的實(shí)例。


Creating Documents

在顯示文檔列表之前,您需要至少添加一個(gè)文檔才能查看。 在此應(yīng)用中創(chuàng)建新文檔需要做三件事:

  • 1) 存儲(chǔ)條目。
  • 2) 查找可用的URL。
  • 3) 創(chuàng)建文檔document。

1. Storing Entries

如果您在應(yīng)用程序中創(chuàng)建條目,您將在單元格中看到創(chuàng)建日期。 您希望顯示有關(guān)文檔的信息,例如縮略圖或您自己的文本,而不是顯示日期。

所有這些信息都保存在另一個(gè)名為Entry的類中。 每個(gè)Entry由表視圖中的單元格表示。

首先,打開Entry.swift并替換類實(shí)現(xiàn) - 但不是Comparable擴(kuò)展! - 用:

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只是跟蹤上面討論的所有項(xiàng)目。 確保你沒有刪除Comparable!

此時(shí),您將看到編譯器錯(cuò)誤,因此您必須稍微清理代碼。

現(xiàn)在,轉(zhuǎn)到ViewController.swift并刪除這段代碼。 你稍后會(huì)替換它:

private func addOrUpdateEntry() {
  let entry = Entry()
  entries.append(entry)
  tableView.reloadData()
}

由于您剛剛刪除了addOrUpdateEntry,因此您將看到另一個(gè)編譯器錯(cuò)誤:

刪除addEntry(_ :)中調(diào)用addOrUpdateEntry()的行。

2. Finding an Available URL

下一步是找到要在其中創(chuàng)建文檔的URL。 這并不像聽起來那么容易,因?yàn)槟阈枰詣?dòng)生成一個(gè)尚未采用的文件名。 首先,您將檢查文件是否存在。

轉(zhuǎn)到ViewController.swift。 在頂部,您將看到兩個(gè)屬性:

private var selectedEntry: Entry?
private var entries: [Entry] = []
  • selectedEntry將幫助您跟蹤用戶正在與之交互的條目。
  • entries是一個(gè)包含磁盤上所有條目的數(shù)組。

要檢查文件是否存在,請查看entries以查看是否已使用該名稱。

現(xiàn)在,再添加兩個(gè)屬性:

private lazy var localRoot: URL? = FileManager.default.urls(
                                     for: .documentDirectory, 
                                     in: .userDomainMask).first
private var selectedDocument: Document?

localRoot實(shí)例變量跟蹤文檔的目錄。 selectedDocument將用于在主視圖控制器和詳細(xì)視圖控制器之間傳遞數(shù)據(jù)。

現(xiàn)在,在viewDidLoad()下添加此方法以返回特定文件名的文件的完整路徑:

private func getDocumentURL(for filename: String) -> URL? {
  return localRoot?.appendingPathComponent(filename, isDirectory: false)
}

然后在其下添加一個(gè)檢查文件名是否已存在的方法:

private func docNameExists(for docName: String) -> Bool {
  return !entries.filter{ $0.fileURL.lastPathComponent == docName }.isEmpty
}

如果文件名已存在,則需要查找新文件名。

因此,添加一個(gè)方法來查找未采用的名稱:

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 :)以傳入的文檔名稱開頭,并檢查它是否可用。 如果沒有,它會(huì)在名稱末尾添加1并再次嘗試,直到找到可用名稱。

3. Creating a Document

創(chuàng)建Document有兩個(gè)步驟。 首先,使用URL初始化Document以將文件保存到。 然后,調(diào)用saveToURL以保存文件。

創(chuàng)建文檔后,需要更新對(duì)象數(shù)組以存儲(chǔ)文檔并顯示詳細(xì)視圖控制器。

現(xiàn)在在indexOfEntry(for :)下面添加此代碼,以查找特定fileURL的條目索引:

private func indexOfEntry(for fileURL: URL) -> Int? {
  return entries.firstIndex(where: { $0.fileURL == fileURL }) 
}

接下來,添加一個(gè)方法來添加或更新下面的條目:

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的條目索引。 如果存在,則更新其屬性。 如果沒有,則創(chuàng)建一個(gè)新Entry。

最后,添加一個(gè)插入新文檔的方法:

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) 向表中添加條目。

現(xiàn)在,將以下內(nèi)容添加到addEntry(_ :)以調(diào)用您的新代碼:

insertNewDocument()

4. Final Changes

你幾乎準(zhǔn)備好測試一下了!

找到tableView(_:cellForRowAt :)并將單元格配置替換為:

cell.photoImageView?.image = entry.metadata?.image
cell.titleTextField?.text = entry.description
cell.subtitleLabel?.text = entry.version.modificationDate?.mediumString

構(gòu)建并運(yùn)行您的項(xiàng)目。 您現(xiàn)在應(yīng)該可以點(diǎn)擊+按鈕來創(chuàng)建存儲(chǔ)在文件系統(tǒng)中的新文檔documents

如果查看控制臺(tái)輸出,您應(yīng)該看到顯示保存文檔的完整路徑的消息,如下所示:

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

但是,這個(gè)應(yīng)用程序有一個(gè)大問題。 如果您再次構(gòu)建并運(yùn)行該應(yīng)用程序,列表中不會(huì)顯示任何內(nèi)容!

那是因?yàn)檫€沒有列出文件的代碼。 你現(xiàn)在就加上。


Listing Local Documents

要列出本地文檔,您將獲取本地Documents目錄中所有文檔的URL并打開每個(gè)文檔。 您將讀取元數(shù)據(jù)以獲取縮略圖而不是數(shù)據(jù),因此保持高效。 然后,您將再次關(guān)閉它并將其添加到表視圖中。

ViewController.swift中,您需要添加在給定文件URL的情況下加載文檔的方法。 在viewDidLoad()下面添加此權(quán)限:

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

在這里打開文檔,獲取創(chuàng)建條目所需的信息并顯示縮略圖。 然后再將其關(guān)閉而不是保持打開狀態(tài)。 這有兩個(gè)重要原因:

  • 1) 當(dāng)您只需要一個(gè)部件時(shí),它可以避免將整個(gè)UIDocument保留在內(nèi)存中的開銷。
  • 2) UIDocuments只能打開和關(guān)閉一次。 如果要再次打開相同的fileURL,則必須創(chuàng)建新的UIDocument實(shí)例。

添加這些方法以在剛剛添加的方法下執(zhí)行刷新:

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目錄中的所有文件,并使用應(yīng)用程序的文件擴(kuò)展名加載每個(gè)文檔。

現(xiàn)在,您需要將以下內(nèi)容添加到viewDidLoad()的底部,以便在應(yīng)用啟動(dòng)時(shí)加載文檔列表:

refresh()

建立并運(yùn)行。 現(xiàn)在,您的應(yīng)用程序應(yīng)該正確地選擇自上次運(yùn)行以來的文檔列表。


Creating Actual Entries

現(xiàn)在是時(shí)候?yàn)?code>PhotoKeeper創(chuàng)建真正的條目了。 添加照片有兩種情況:

  • 1) 添加新條目。
  • 2) 編輯舊條目。

這兩種情況都將呈現(xiàn)DetailViewController。 但是,當(dāng)用戶想要編輯條目時(shí),您將把該文檔從ViewController上的selectedDocument屬性傳遞到DetailViewController上的document屬性。

仍然在ViewController.swift中,添加一個(gè)方法,在insertNewDocument(with:title:)下面顯示詳細(xì)視圖控制器:

private func showDetailVC() {
  guard let detailVC = detailVC else { return }
  
  detailVC.delegate = self
  detailVC.document = selectedDocument
  
  mode = .viewing
  present(detailVC.navigationController!, animated: true, completion: nil)
}

如果可能,在這里訪問計(jì)算屬性detailVC,并傳遞selectedDocument(如果存在)。 如果它是nil,那么你知道你正在創(chuàng)建一個(gè)新文檔。 mode = .viewing讓視圖控制器知道它正在查看而不是編輯模式。

現(xiàn)在轉(zhuǎn)到UITableViewDelegate擴(kuò)展并實(shí)現(xiàn)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屬性并顯示詳細(xì)視圖控制器。

現(xiàn)在將addEntry(_ :)實(shí)現(xiàn)替換為:

selectedEntry = nil
selectedDocument = nil
showDetailVC()

在此處清空selectedEntryselectedDocument,然后顯示詳細(xì)視圖控制器以指示您要?jiǎng)?chuàng)建新文檔。

建立并運(yùn)行。 現(xiàn)在嘗試添加一個(gè)新條目。

看起來不錯(cuò),但是點(diǎn)擊Done時(shí)沒有任何反應(yīng)。 是時(shí)候解決了!

條目由標(biāo)題和兩個(gè)圖像組成。 用戶可以在文本字段中鍵入標(biāo)題,并在點(diǎn)擊Add/Edit Photo按鈕后通過與UIImagePickerController交互來選擇照片。

轉(zhuǎn)到DetailViewController.swift。

首先,您需要實(shí)現(xiàn)openDocument()。 它在viewDidLoad()的末尾被調(diào)用,以最終打開文檔并訪問完整大小的圖像。 將此代碼添加到openDocument()

if document == nil {
  showImagePicker()
}
else {
  document?.open() { [weak self] _ in
    self?.fullImageView.image = self?.document?.photo?.mainImage
    self?.titleTextField.text = self?.document?.description
  }
}

打開文檔后,將存儲(chǔ)的圖像分配給fullImageView,將文檔的description分配為標(biāo)題。


Store and Crop

當(dāng)用戶選擇他們的圖像時(shí),UIImagePickerController返回imagePickerController(_:didFinishPickingMediaWithInfo:)中的信息。

此時(shí),您希望將所選圖像分配給fullImageView,創(chuàng)建縮略圖并將完整圖像和縮略圖圖像保存在各自的局部變量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框架創(chuàng)建縮略圖。 而不是必須弄清楚要裁剪的圖像最相關(guān)的矩形是你自己,這兩個(gè)框架為你做了!

事實(shí)上,縮略圖看起來與Photos庫中的縮略圖完全相同:


Compare and Save

最后,您需要實(shí)現(xiàn)用戶點(diǎn)擊Done按鈕時(shí)發(fā)生的情況。

所以,用以下內(nèi)容更新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)
}

確保存在適當(dāng)?shù)膱D像后:

  • 1) 通過將新圖像與文檔進(jìn)行比較,檢查圖像或標(biāo)題是否有變化。
  • 2) 如果未傳遞現(xiàn)有文檔,則將控制權(quán)交給代理(主視圖控制器)。
  • 3) 如果您確實(shí)傳遞了一個(gè)文檔,那么首先保存并覆蓋它,然后讓代理發(fā)揮其魔力。

Insert or Update

最后一個(gè)難題是在主視圖控制器上插入或更新這些新數(shù)據(jù)。

轉(zhuǎn)到ViewController.swift并找到DetailViewControllerDelegate擴(kuò)展并實(shí)現(xiàn)空委托方法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)

這是你添加的內(nèi)容:

  • 1) 如果詳細(xì)視圖控制器沒有文檔,則插入一個(gè)新文檔。
  • 2) 如果文檔存在,則只需更新舊條目。

現(xiàn)在,構(gòu)建并運(yùn)行以查看此操作:

成功! 您最終可以創(chuàng)建正確的條目甚至編輯照片! 但是,如果您嘗試更改標(biāo)題或刪除條目,則更改將只是暫時(shí)的,并在您退出并打開應(yīng)用程序時(shí)返回。


Deleting and Renaming

對(duì)于刪除和重命名文檔,您將使用FileManager,它允許您訪問共享文件管理器對(duì)象,該對(duì)象允許您與文件系統(tǒng)的內(nèi)容進(jìn)行交互并對(duì)其進(jìn)行更改。

首先,返回ViewController.swift并將delete(entry :)的實(shí)現(xiàn)更改為:

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

要?jiǎng)h除,請使用FileManagerremoveItem(at :)方法。 在構(gòu)建和運(yùn)行時(shí),您會(huì)看到現(xiàn)在可以滑動(dòng)行以永久刪除它們。 請務(wù)必關(guān)閉并重新啟動(dòng)應(yīng)用以驗(yàn)證它們是否已經(jīng)消失。

接下來,您將添加重命名文檔的功能。

首先,添加以下代碼到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()

對(duì)于重命名,使用FileManagermoveItem(at:to :)方法。 上述方法中的其他所有內(nèi)容都是您的普通表視圖管理。 很簡單,嗯?

最后要做的是檢查用戶是否在detailViewControllerDidFinish(_:with:title :)中更改了文檔的標(biāo)題。

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

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

最后,構(gòu)建并運(yùn)行以嘗試這種存儲(chǔ)照片的真棒新方法!

如果您有興趣深入創(chuàng)建自己的文檔和管理文件,請查看Apple有關(guān)UIDcocumentFileManager的文檔

后記

本篇主要講述了UIDocument的數(shù)據(jù)存儲(chǔ),感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容