UIDocument From Scratch
Learn how to add document support to your app using UIDocument. By Lea Marolt Sonnenschein.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
UIDocument From Scratch
30 mins
Creating Documents
Before you can display a list of documents, you need to be able to add at least one so that you have something to look at. There are three things you need to do to create a new document in this app:
- Store entries.
- Find an available URL.
- Create the document.
Storing Entries
If you create an entry in the app, you’ll see the creation date in the cell. Instead of displaying dates, you want to display information about your documents, like the thumbnail or your own text.
All of this information is held in another class called Entry
. Each Entry
is represented by a cell in the table view.
First, open Entry.swift and replace the class implementation — but not the Comparable
extension! — with:
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
simply keeps track of all the items discussed above. Make sure you don’t delete the Comparable
!
At this point, you’ll see a compiler error, so you have to clean the code up a bit.
Now, go to ViewController.swift and remove this piece of code. You’ll replace it later:
private func addOrUpdateEntry() {
let entry = Entry()
entries.append(entry)
tableView.reloadData()
}
Since you just removed addOrUpdateEntry
, you’ll see another compiler error:
Delete the line calling addOrUpdateEntry()
in addEntry(_:)
.
Finding an Available URL
The next step is to find a URL where you want to create the document. This isn’t as easy as it sounds because you need to auto-generate a filename that isn’t already taken. First, you’ll check if a file exists.
Go to ViewController.swift. At the top, you’ll see two properties:
private var selectedEntry: Entry?
private var entries: [Entry] = []
-
selectedEntry
will help you keep track of the entry the user is interacting with. -
entries
is an array that contains all the entries on the disk.
To check if a file exists, look through entries
to see if the name is already taken.
Now, add two more properties below:
private lazy var localRoot: URL? = FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask).first
private var selectedDocument: Document?
The localRoot
instance variable keeps track of the document’s directory. The selectedDocument
will be used for passing data between the master and detail view controllers.
Now, under viewDidLoad()
add this method to return the full path for a file for a specific filename:
private func getDocumentURL(for filename: String) -> URL? {
return localRoot?.appendingPathComponent(filename, isDirectory: false)
}
Then under that, add a method that checks whether the filename already exists:
private func docNameExists(for docName: String) -> Bool {
return !entries.filter{ $0.fileURL.lastPathComponent == docName }.isEmpty
}
If the filename does already exist, you want to find a new one.
So, add a method to find a non-taken name:
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:)
starts with the document name passed in and checks if it is available. If not, it adds 1 to the end of the name and tries again until it finds an available name.
Creating a Document
There are two steps to create a Document
. First, initialize the Document
with the URL to save the file to. Then, call saveToURL
to save the files.
After you create the document, you need to update the objects array to store the document and display the detail view controller.
Now add this code below indexOfEntry(for:)
to find the index of an entry for a specific fileURL
:
private func indexOfEntry(for fileURL: URL) -> Int? {
return entries.firstIndex(where: { $0.fileURL == fileURL })
}
Next, add a method to add or update an entry below:
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:)
finds the index of an entry for a specific fileURL
. If it exists, it updates its properties. If it doesn’t, it creates a new Entry
.
Finally, add a method to insert a new document:
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)
}
}
}
You’re finally putting all the helper methods you wrote to good use. Here, the code you added:
- Finds an available file URL in the local directory.
- Initializes a
Document
. - Saves the document right away.
- Adds an entry to the table.
Now, add the following to addEntry(_:)
to call your new code:
insertNewDocument()
Final Changes
You’re almost ready to test this out!
Find tableView(_:cellForRowAt:)
and replace the cell configuration with:
cell.photoImageView?.image = entry.metadata?.image
cell.titleTextField?.text = entry.description
cell.subtitleLabel?.text = entry.version.modificationDate?.mediumString
Build and run your project. You should now be able to tap the + button to create new documents that get stored on the file system:
If you look at the console output, you should see messages showing the full paths of where you’re saving the documents, like this:
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
However, this app has a big problem. If you build and run the app again, nothing will show up in the list!
That’s because there is no code to list documents yet. You’ll add that now.