SwiftUI Field Notes: DocumentGroup

When you build a SwiftUI-lifecycle-based app for macOS or iOS, you’re expected to define a Scene – the first of which generally defines what (and how) that App displays when it is launched.

DocumentGroup is one of the types of Scene (for macOS and iOS platforms) that is focused on an App lifecycle built around viewing, creating, and editing documents. The common alternative, WindowGroup, doesn’t prevent you from reading, editing, or saving documents, but does change what happens when an App launches. The difference in “how it works” is when you launch an app using DocumentGroup, the app is expecting that the first thing you’ll want to do is either open a document or create a new one. It does this by kicking off an “open” dialog on macOS, or displaying a DocumentBrowser on iOS, configured based on the types your app says it knows how to create (and/or open). WindowGroup, by comparison, just tosses up whatever view structure you define when the app starts – not expecting you to want to start from the concept of “I want to open this document” or “I want to create an XXX document”.

You can define more than one DocumentGroup within the SceneBuilder for your app. You might do this, for example, to support opening and viewer or editing more than one file type. Only the first DocumentGroup that you define (and its associated file type) is linked to the “create new document” of the system provided Document Browser (or the File > New... menu within a macOS app). You’re responsible for exposing and/or providing an interface to create any new file types of additional file types.

Document Model Choices

When you define a DocumentGroup, provide it with a view that’s either the editor or viewer for a document type. That view is expected to conform to one of two protocols: FileDocument or ReferenceFileDocument. The gist of these protocols being that your editor (or viewer) view gets passed a configuration object that represents the document as a wrapper around your data model, and has with it a binding or reference to an ObservableObject, depending on which protocol you choose. Use FileDocument when you have a struct based data model, and ReferenceFileDocument when your data model is based on a class (and conforms to ObservableObject).

Apple provides a sample code entitled Building a Document-Based App with SwiftUI that uses the ReferenceFileDocument, and Paul Hudson has the article How to create a document-based app using FileDocument and DocumentGroup that shows using FileDocument.

The Default Call To Action

Be aware when designing and building an App using DocumentGroup that there’s no easy place to tie into the life cycle to provide a clear call to action before the system attempts to open a file as your app launches. The place where your code (and any control you have) kicks off is after the app has presented a file open/new view construct – either a DocumentBrowser (iOS) or the system Open dialog box (macOS). I find that experience can be confusing for anyone new to using the app, as there’s no easy way to introduce what the app is, what’s new, or provide instruction on how to use the app until after the person has chosen a document to open or created a new document.

If you need (or want) to provide a call to action, or to get some critical information established that you need for creating a new document, you need to capture that detail after the new document is passed to you, but potentially before you display any representation of the document. That in turn implies that a “new” document model that requires explicit parameters won’t work for conforming to FileDocument or ReferenceDocument. You are responsible for making sure any critical details get established (and presumably filled in) after the new document is created.

For example, if a new document requires some value (or values), add a function or computed property that returns a Boolean that indicates if the values exist or not. Then in your editor view, conditionally return a view that 1) captures and stores those initial values into the new document when they don’t yet exist or 2) displays the document if those values are already defined.

NavigationView (or not)

An interesting side effect of DocumentGroup is that the views you provide to it get very different environments based on platform. iOS (and Mac catalyst based apps) display the view you provide enclosed within a NavigationView that the document browser provides. With macOS, there is no default NavigationView. If you want iOS and macOS to behave with a similar design (for example, a two-column style navigational layout), you need to provide your own NavigationView for the macOS version.

I find NavigationView to be a quirky, somewhat confusing setup (I’m glad its deprecated with iOS 16 and macOS 13 in favor of the lovely newer navigation types). When you’re working with iOS, it may not be obvious that you can create a tuple-view to provide the two separate views that you need to enclose within NavigationView to get that two-column view layout. You can do this by providing a view that returns two separate views without an enclosing grouping or container view. For example, the following view declaration provides a two-tuple view (with SidebarView and ContentView) that the NavigationView provided by the built-in document browser will use when displaying your document.

var body: some View {
   SidebarView()
   ContentView()
}

If you provide your own NavigationView inside the view that the DocumentBrowser vends, you get a rather unfortunate looking “double navigation” view setup. Instead, provide a tuple of views based on the navigation style you’d like your editor (or viewer) to display.

If you’re used to NavigationView, but ready to target these later platform versions, definitely take the time to read Migrating to New Navigation Types and watch related WWDC22 talks.

Published by heckj

Developer, author, and life-long student. Writes online at https://rhonabwy.com/.