UIDocument From Scratch

Learn how to add document support to your app using UIDocument. By Lea Marolt Sonnenschein.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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:

  1. Store entries.
  2. Find an available URL.
  3. 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:

  1. Finds an available file URL in the local directory.
  2. Initializes a Document.
  3. Saves the document right away.
  4. 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.