MacKuba

Kuba Suder's blog on Mac & iOS development

WWDC 20

Build document-based apps in SwiftUI

Categories: SwiftUI 0 comments Watch the video

Document-based apps are apps whose primary workflow is opening user's files of a specific type from the disk, editing them and saving them back to disk – as opposed to apps that work with data kept in their own internal databases

There are also apps of mixed type, where document-focused features are a part of the UI, e.g.:

  • Xcode is primarily document-based, but it also has non-document parts like documentation, organizer
  • Mail generally works with an internally managed database, but can also open single email files from any place on disk

For document based apps, use the DocumentGroup scene as the main scene of the app:

DocumentGroup(newDocument: MyDocument()) { file in
  TextEditor(text: file.$document.text)
}

You can also mix DocumentGroup and WindowGroup in apps that should have both document-based and non-document-based windows

DocumentGroup automatically provides various document management features expected on the given platform:

  • menu items like File > Open/Save in the macOS menu
  • state tracking and file renaming/moving using the titlebar popup on macOS
  • Handoff between devices
  • a document browser and a navigation bar with search field on iOS

The DocumentGroup initializer takes a document object which represents a single document of a type handled by the given document group, and a view closure that describes the view hierarchy to be rendered for each document's scene

The closure's argument (file) gives you a binding that provides read-write data to the document's data model

The binding lets SwiftUI know when you update the document contents through the binding, so it can automatically handle things like registering undo actions and marking the document state as dirty

TextEditor() – a new built-in text view type kind of like UITextView

Document types

Document-based apps have a “Document Types” section in the Info.plist, where you declare Uniform Type Identifiers of document types associated with your app

Imported types  ⭢  documents from other apps or sources that your app is able to open

Exported types  ⭢  document types owned by your app

Document object

The type that implements the document model conforms to FileDocument (for value types) and declares its own UTType instances that represent imported and exported file types:

import UniformTypeIdentifiers

extension UTType {
    static var exampleText: UTType {
        UTType(importedAs: "com.example.plain-text")
    }

    static let shapeEditDocument =
        UTType(exportedAs: "com.example.ShapeEdit.shapes")
}

An imported type needs to be a computed property, because the value returned from the constructor may change between calls while the app is running, depending on system configuration. Exported type can just be assigned once and stored.

→ this is slightly better explained in a tech talk video which talks specifically about UTIs, "Uniform Type Identifiers — a reintroduction"

The document type provides a list of types (public types and app's own types) that it can accept:

static var readableContentTypes: [UTType] { [.exampleText] }

It also has methods for reading and writing its document to/from a file, which you need to implement, using e.g. Codable to encode/decode the value into your chosen format:

init(fileWrapper: FileWrapper, contentType: UTType) throws { ... }

func write(to fileWrapper: inout FileWrapper, contentType: UTType) throws { ... }

In those methods, you can assume that the content type is one of those you declared as accepted by your app.