SwiftUI developers building for macOS need a reliable way to know when their app's window has focus. The old approach—using controlActiveState—has officially been retired. Apple replaced it with appearsActive, and if you're still using the old method, your code is living on borrowed time.
What happened
- ControlActiveState is deprecated as of macOS 15
- AppearsActive is the replacement environment value for detecting focused windows
- Set to true when the window is key, false when it loses focus
- Works with onChange to respond to focus transitions
- Different from scenePhase, which tracks app-wide lifecycle state
The Deprecation Story
For years, controlActiveState sat quietly in SwiftUI's environment without much fanfare. It worked, sort of, but developers often found its behavior inconsistent across different macOS versions. Some reported it flickering between states during rapid window switches. Others noticed it didn't always reflect the true focus state when sheets or alerts were involved.
Apple has now formally deprecated controlActiveState and introduced appearsActive as its direct replacement. The new environment value provides the same information—window focus state—but does so more reliably and with clearer semantics.
How appearsActive Works
The appearsActive environment value belongs to the environment object that SwiftUI injects into every view hierarchy. When you declare it as a property, SwiftUI automatically populates it with the current focus state.
“`swift
@Environment(\.appearsActive) var appearsActive
“`
This value reads as true whenever the window containing your view is the key window—the one receiving keyboard input and mouse events. When another window takes over, it flips to false. The change is immediate and reflects exactly what macOS reports about window focus.
Responding to Focus Changes
Static focus information is only half the battle. You'll usually want to do something when focus changes—refresh data, update a control, or adjust your UI. The onChange modifier handles this elegantly:
“`swift
.onChange(of: appearsActive) { oldValue, newValue in
Let oldStatus = oldValue ? "active" : "inactive"
Let newStatus = newValue ? "active" : "inactive"
Print("Window focus changed from \(oldStatus) to \(newStatus).")
}
“`
The closure gives you both the previous and current values, so you can branch your logic based on direction—activating something on focus gain, pausing it on focus loss.
Why This Matters for Your Apps
Window focus detection sounds like a niche concern, but it touches many common patterns. Settings windows often need to sync state with a main window. Floating panels might want to hide when the user switches away. Status indicators should reflect the actual state of the window they're attached to.
Without reliable focus detection, developers resort to workarounds—timer-based polling, NotificationCenter observers for window changes, or simply ignoring the problem and letting their UI drift out of sync. AppearsActive gives you a clean, SwiftUI-native way to handle this without leaving the declarative paradigm.
Practical Example: Settings Sync
Say you're building a macOS app with a floating preferences window. The main window holds user data, and the preferences window controls sync settings. When the preferences window loses focus, you want to pause any ongoing sync to save resources. When it regains focus, you resume.
“`swift
Struct PreferencesView: View {
@Environment(\.appearsActive) var isActive
@State private var syncPaused = false
Var body: some View {
VStack {
Toggle("Sync automatically", isOn: $syncPaused)
// Other preferences…
}
.onChange(of: isActive) { wasActive, isNowActive in
If wasActive && !isNowActive {
PauseSync()
} else if !wasActive && isNowActive {
ResumeSync()
}
}
}
}
“`
This pattern keeps your sync logic cleanly separated from the view layer while responding immediately to user behavior.
AppearsActive vs Scene Phase
Don't confuse appearsActive with scenePhase. Both relate to window and app state, but they track different things.
ScenePhase reports whether your app is in the foreground, background, or inactive at the application level. It doesn't distinguish between multiple windows or tell you which window is key.
AppearsActive is scoped to individual windows. If your app has three windows open, each can independently report its focus state. This granularity matters for complex document-based apps or anything with auxiliary panels.
Use scenePhase for app-wide concerns like saving state on deactivation. Use appearsActive for window-specific UI updates and behavior.
What to Watch Next
Apple hasn't signaled further changes to this API, but keep an eye on future betas for any refinements to appearsActive behavior, especially around sheet presentation and modal windows. If you're maintaining older code using controlActiveState, now's the time to migrate—macOS 15 is already in wide testing, and the deprecation won't last forever.
Key takeaways
- AppearsActive replaces the deprecated controlActiveState for detecting focused windows
- Use @Environment(\.appearsActive) to read the current focus state
- Pair with onChange to respond to focus transitions in real time
- Different from scenePhase: appearsActive is window-scoped, scenePhase is app-scoped
- Migrate existing code now before macOS 15 reaches stable release
Frequently asked questions
Can I use appearsActive on iOS or iPadOS?
No, appearsActive is currently macOS-only. IOS and iPadOS apps don't have the concept of window focus the same way desktop apps do.
Does appearsActive work with NSSplitView or NSTabView?
Yes, it tracks the containing window's focus state regardless of internal view structure.
What happens if my view is inside a sheet or popover?
The sheet or popover has its own window, so appearsActive reflects whether that overlay is key rather than the main window behind it.
Is there a SwiftUI equivalent for AppKit's NSWindowDidBecomeKeyNotification?
AppearsActive essentially replaces that notification in SwiftUI's declarative model, with onChange serving the role of an observer.
Will controlActiveState stop working entirely?
Deprecated APIs typically continue working for one or two macOS releases, but Apple removes them eventually. Plan to migrate before macOS 16.