版本記錄
版本號 | 時間 |
---|---|
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的數據存儲,感興趣的給個贊或者關注~~~