Add an Open Recent Menu to a SwiftUI app
Why would you need to add an Open Recent menu? The most common reason is you created an app that doesn’t use the document architecture, and you want a way for your app’s users to access recent items.
Adding an Open Recent menu requires you to do the following things:
- Use the
NSDocumentControllerclass to track recent items. - Add an
@Observableclass to hold the recent items so SwiftUI can update the Open Recent menu when its contents change.
Using NSDocumentController to track recent items
The name NSDocumentController implies that it works with NSDocument. However, the parts of NSDocumentController that deal with the Open Recent menu require only an array of URLs. You can use NSDocumentController without creating a document app.
Use the shared instance to access the standard document controller.
NSDocumentController.shared
Use the recentDocumentURLs property to access the URLs of the recent items and fill the Open Recents menu. Call the noteNewRecentDocumentURL function to update the recent items after opening or creating an item. Call the clearRecentDocuments function to clear the recent items list and clear the Open Recent menu.
Initial version of the Open Recent menu
An initial version of an Open Recent menu looks like the following code:
struct OpenRecentMenu: View {
@Environment(\.openWindow) private var openWindow
var body: some View {
Menu("Open Recent") {
ForEach(NSDocumentController.shared.
recentDocumentURLs, id: \.self) { url in
Button(url.lastPathComponent) {
// Item is a class in your app.
let itemToOpen = Item(location: url)
openWindow(value: itemToOpen)
NSDocumentController.shared.
noteNewRecentDocumentURL(url)
}
}
Divider()
Button("Clear Menu") {
NSDocumentController.shared.
clearRecentDocuments(nil)
}
}
}
}
The code creates the Open Recent menu and fills it with recent items. There is one big problem. The Open Recent menu doesn’t update until you restart the app.
The reason the menu doesn’t update is SwiftUI doesn’t automatically update the menu when the shared document controller’s array of recent items changes.
To get the menu to update, you must add an @Observable class that holds the recent item URLs and pass that class to the menu view.
Create the @Observable class
The class has two requirements. First, it needs a property that holds the URLs of the recent items. Second, it needs a function to update the array from the shared document controller.
@Observable
class RecentItems {
@MainActor static let shared = RecentItems()
var items: [URL] = []
init(items: [URL]) {
self.items = items
}
@MainActor func refresh() {
let docControllerRecents = NSDocumentController.
shared.recentDocumentURLs
if docControllerRecents != items {
items = docControllerRecents
}
}
}
Supply the value NSDocumentController.shared.recentDocumentURLs to the RecentItems initializer to fill the Open Recent menu when your app launches.
When you open or add an item in your app, call the refresh function to update the Open Recent menu.
Add the @Observable class to the Menu
To use your @Observable class in the menu, you must add a @State property for the class to the App struct. Pass the value to the menu using a binding. Use the array in your @Observable class to fill the Open Recent menu.
The following code shows the updated Open Recent menu:
struct OpenRecentMenu: View {
@Environment(\.openWindow) private var openWindow
@Binding var recents: RecentItems
var body: some View {
Menu("Open Recent") {
ForEach(recents.items, id: \.self) { url in
Button(url.lastPathComponent) {
let item = Item(location: url)
openWindow(value: item)
NSDocumentController.shared.
noteNewRecentDocumentURL(url)
recents.refresh()
}
}
Divider()
Button("Clear Menu") {
NSDocumentController.shared.
clearRecentDocuments(nil)
recents.refresh()
}
}
}
}