AppKit Window Restoration: Reopen the Right Workspace, Not Just a Window
Implement AppKit window restoration with stable IDs, controller recreation, bounded saved state, migration defaults, and tested multiwindow relaunch behavior.
Window restoration is useful only when an application can recreate the work the window represented. Reopening a blank window at the old coordinates is not continuity if the user was editing a particular document, inspecting a selected object, or working across several related windows. AppKit’s restoration system can preserve window configuration and let an application recreate the corresponding controller and domain objects. The application still has to define stable identity, encode the small amount of state needed to resume, handle missing data, and decide when restoration should be skipped.
This article describes AppKit restoration for macOS windows. It does not describe iOS scene restoration or SwiftUI’s separate state restoration APIs. Treat saved state as a recoverable hint rather than a guarantee: files can move, accounts can change, extensions can disappear, and a window type can evolve between application versions.
Give each restorable window a stable identity
AppKit needs to know which kind of window it should recreate. Give every window a stable NSUserInterfaceItemIdentifier, a frame autosave name if the window’s geometry should persist, and a restoration class capable of constructing the relevant controller. Set isRestorable intentionally instead of relying on a style-mask default that may change when the window type changes. Document windows managed by NSDocumentController have an existing integration path; custom inspectors and utility windows usually need an explicit restoration owner.
The identifier describes the window role, not a unique saved instance. If a user can open several windows of the same kind, persist a separate stable domain identifier for each instance, such as a document URL or object UUID. Do not use a transient array index or window title as identity. Titles are localized and editable; indices change as windows close.
import AppKit
@MainActor
func configureRestorableWindow(_ window: NSWindow) {
window.identifier = NSUserInterfaceItemIdentifier("AssetInspector")
window.setFrameAutosaveName("AssetInspectorFrame")
window.restorationClass = AssetInspectorRestoration.self
window.isRestorable = true
}
Frame persistence and application-level workspace state are separate concerns. A saved frame says where the window was, not what content it displayed. On displays that were disconnected, resolution or scale may differ. Allow the system to restore usable geometry, then validate that the resulting frame intersects a current screen’s visible frame and adjust only when necessary. Do not blindly override the restored frame on every launch, or user resizing will never persist.
Restoration class owns reconstruction
When AppKit asks a restoration class for a window, the class should create or locate the objects normally responsible for managing that window. Usually that means a window controller and possibly a document or model coordinator. Retain the created controller for as long as the window is expected to live; returning a window whose controller is immediately deallocated can cause subtle lifecycle failures.
The restoration callback should distinguish known window identifiers and treat an unknown or obsolete identifier as a recoverable case. Return no window when the old state cannot be reconstructed, and let the application open its normal default interface. Avoid network requests, authentication prompts, or long database scans synchronously inside the restoration callback. Create the minimum valid window, display a progress or recovery state if loading is asynchronous, and deliver the restored content after validating that the request is still current.
The app delegate must also opt in to secure restorable state when the app’s restoration archive supports secure coding. Keep restoration decoding restricted to the expected classes and validate decoded identifiers before using them; opting in is not a reason to decode arbitrary object graphs.
import AppKit
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
true
}
}
import AppKit
@MainActor
final class AssetInspectorRestoration: NSObject, NSWindowRestoration {
static func restoreWindow(
withIdentifier identifier: NSUserInterfaceItemIdentifier,
state: NSCoder,
completionHandler: @escaping (NSWindow?, Error?) -> Void
) {
guard identifier.rawValue == "AssetInspector" else {
completionHandler(nil, nil)
return
}
let controller = AssetInspectorWindowController()
ApplicationCoordinator.shared.retain(controller)
completionHandler(controller.window, nil)
}
}
The coordinator in this illustrative example must have a real ownership policy, such as a window-controller registry that removes a controller when its window closes. A process-wide array that never removes controllers leaks windows and retains their model state. Restoration is not an excuse to create two controllers for one document; look up existing domain objects by stable identifier before allocating duplicates.
Encode only durable, bounded state
Window controllers can encode and restore app-specific state through the state coder. Store identifiers, selected tab identifiers, a navigation location, and small presentation preferences. Avoid archiving entire model graphs, caches, credentials, file contents, or objects that can be reconstructed from the document. A compact identifier can be checked against the current store at launch; a large archive can become stale, expensive, or incompatible after a schema change.
Treat restoration data as untrusted input even when the operating system wrote it. Decode expected primitive types and supported versions, validate every identifier, bound collection lengths and string sizes, and use safe defaults when keys are absent. Do not force-cast decoded values or assume an index remains valid. If a selected tab was removed in a later release, choose a documented default rather than restoring an invalid selection.
Inside the window-controller subclass, encode only the durable values the app needs to re-resolve:
private enum RestorationKey {
static let selectedRecord = "selected-record-id"
static let selectedTab = "selected-tab"
}
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
if let id = viewModel.selectedRecordID {
coder.encode(id.uuidString, forKey: RestorationKey.selectedRecord)
}
coder.encode(viewModel.selectedTab.rawValue, forKey: RestorationKey.selectedTab)
}
override func restoreState(with coder: NSCoder) {
super.restoreState(with: coder)
if let raw = coder.decodeObject(of: NSString.self, forKey: RestorationKey.selectedRecord) as String?,
let id = UUID(uuidString: raw), viewModel.containsRecord(id) {
viewModel.selectRecord(id)
}
viewModel.selectTab(rawValue: coder.decodeInteger(forKey: RestorationKey.selectedTab))
}
This sketch assumes the view model supplies a validated tab selection API. In a real application, check whether a key exists when zero is a meaningful value, and version the application-owned payload if its meaning may change. Secure coding rules depend on the types and SDK contract in use; enable only the supported restoration coding path for the app and avoid accepting arbitrary classes from an archive.
Restore relationships in dependency order
A workspace may contain a main window, document windows, inspectors, and a panel tied to the active document. Restoration can ask for multiple windows. The application needs a coherent way to reconstruct relationships: create or locate the document first, then attach its inspector; identify the owning window by stable document identity; and ensure a restored panel does not bind to whichever document happens to be key by accident.
The order of callbacks is not a substitute for an explicit ownership graph. Persist a stable owner identifier and resolve it through a coordinator. If the referenced document is unavailable, close or show a useful empty state for the dependent window. Avoid presenting duplicate modal sheets before the parent window is visible. If a window depends on a user action or unavailable service, restoration can record intent and defer the dependent UI until the normal prerequisite is met.
Multi-window applications should test closing one window while others remain, reopening several documents, and restoring an inspector whose owner was closed. Document restoration and custom window restoration should not compete to create the same document twice. Keep the window role identifier separate from the per-window domain identifier so an app can restore multiple instances of the same role safely.
Decide what must not be restored
Not every window should return. A transient confirmation panel, one-time onboarding prompt, sensitive temporary workflow, or window bound to an expired resource may be inappropriate to recreate automatically. Make that policy explicit with isRestorable and restoration callback behavior. Never treat the presence of saved state as authorization to repeat an external side effect such as submitting a payment, sending a message, or starting a destructive operation.
For user-created content, restore enough context to let the user resume, but let the authoritative document or service determine current data. A saved selection can be stale; a deleted record should not reappear because its UUID was archived. A saved URL may require a security-scoped bookmark or renewed user authorization; restoration should handle denial as a normal state and offer a clear way to locate the resource again.
Test actual relaunch behavior
Run a release-like app, open the intended combination of documents and auxiliary windows, change selection, resize the workspace, and quit normally. Relaunch and verify that each supported window appears once, its content points to the correct object, selection is valid, and the frame is usable on the current display. Force-quitting may intentionally discard preserved state, so distinguish that system behavior from a restoration regression.
Also test older or incomplete restoration data, a deleted file, unavailable account data, a removed tab, a missing display, a slow model store, and two documents with similar titles. Verify that a restoration failure leaves the application in a useful default state and does not block launch indefinitely. Capture sanitized diagnostics such as window role, schema version, and failure category, not document contents or sensitive user state.
Related:
- NSDocument on macOS: Lifecycle, Autosave, and Version Recovery
- macOS Power Assertions: Preventing Sleep Without Hiding the Reason
Sources: