版本記錄
| 版本號(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添加的UIDocumentBrowserViewController和Files應(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)。
-
FileWrapper:
OS視為單個(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”是NSCoding的key。
現(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)
}
}
PhotoMetadata與PhotoData相同,只是它存儲(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)
fileWrapper是OS文件系統(tǒng)節(jié)點(diǎn),表示包含照片和元數(shù)據(jù)的目錄。 - 3)
photoData和photoMetadata是用于解釋fileWrapper包含的photo.metadata和photo.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ì)象。
最后要做的是為photoData和photoMetadata實(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)
}
在這里,您獲取用戶選擇的條目,填充selectedEntry和selectedDocument屬性并顯示詳細(xì)視圖控制器。
現(xiàn)在將addEntry(_ :)實(shí)現(xiàn)替換為:
selectedEntry = nil
selectedDocument = nil
showDetailVC()
在此處清空selectedEntry和selectedDocument,然后顯示詳細(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)建縮略圖并將完整圖像和縮略圖圖像保存在各自的局部變量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框架創(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除,請使用FileManager的removeItem(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ì)于重命名,使用FileManager的moveItem(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)UIDcocument和FileManager的文檔
后記
本篇主要講述了UIDocument的數(shù)據(jù)存儲(chǔ),感興趣的給個(gè)贊或者關(guān)注~~~
