Follow-up to #25643 (UIScene life-cycle adoption). Non-blocking cleanup — captured here so it isn't lost.
Background
#25643 moved window creation to the first scene connect (WordPressAppDelegate.showInitialUI(in:)). To stay safe if windowManager is touched before any scene has connected (e.g. a background-launch code path), the windowManager lazy getter recovers by asserting and fabricating a window:
if window == nil {
assertionFailure("windowManager accessed before any scene connected")
window = UIWindow(frame: UIScreen.main.bounds)
}
showInitialUI(in:) then carries a matching branch whose only job is to adopt that fabricated window instead of orphaning the UI built into it.
Why it's worth revisiting
- The fabricated window is scene-less (
UIWindow(frame:)), which the rest of the new code treats as invalid — WindowManager.displayOverlayingWindow and CompliancePopoverCoordinator assert a scene-attached main window. It's the only UIWindow(frame:) left in the non-test code.
- The invariant is enforced from both ends — the lazy fabricates,
showInitialUI repairs — ~150 lines apart. Removing one without the other silently turns the repair into dead code or reintroduces an orphan-window bug.
assertionFailure compiles out in release, so a missed early-access path ships as "works by accident" (an invisible, half-built UI) rather than a signal.
- Correctness rests on every pre-scene-connect caller of
windowManager / RootViewCoordinator.shared being guarded by convention rather than by the type system.
The early-access vector is RootViewCoordinator.shared (a static let) evaluating WordPressAppDelegate.shared?.windowManager at first access and caching it for the process lifetime — so the first touch of RootViewCoordinator.shared / .sharedPresenter is what drags windowManager into existence. Its own windowManager property is already WindowManager?, so the consumer already tolerates nil.
Proposed direction
-
Single creation site + explicit optional. Make windowManager an assigned WindowManager?, set when the window is built in showInitialUI. Delete the lazy fallback and the existingWindow adoption branch. Early access becomes a nil callers handle (composes with RootViewCoordinator already taking WindowManager?) instead of a fabricated scene-less window. Ripple is a couple of ? at call sites.
-
Remove the early-access vector (deeper). Stop RootViewCoordinator.shared from eagerly reading windowManager at static-init — resolve it lazily at point-of-use:
private var windowManager: WindowManager? { WordPressAppDelegate.shared?.windowManager }
Then nothing reaches windowManager before the scene exists, the fallback is provably dead and can be deleted outright, and the property could even go back to non-optional.
Files
WordPress/Classes/System/WordPressAppDelegate.swift — windowManager lazy getter, showInitialUI(in:)
WordPress/Classes/System/Root View/RootViewCoordinator.swift — static let shared / sharedPresenter
Surfaced during review of #25643. The PR description already notes that "the separation between the UI and the app process is still not very clean, but that can be tightened in future PRs" — this is part of that.
Follow-up to #25643 (UIScene life-cycle adoption). Non-blocking cleanup — captured here so it isn't lost.
Background
#25643 moved window creation to the first scene connect (
WordPressAppDelegate.showInitialUI(in:)). To stay safe ifwindowManageris touched before any scene has connected (e.g. a background-launch code path), thewindowManagerlazy getter recovers by asserting and fabricating a window:showInitialUI(in:)then carries a matching branch whose only job is to adopt that fabricated window instead of orphaning the UI built into it.Why it's worth revisiting
UIWindow(frame:)), which the rest of the new code treats as invalid —WindowManager.displayOverlayingWindowandCompliancePopoverCoordinatorassert a scene-attached main window. It's the onlyUIWindow(frame:)left in the non-test code.showInitialUIrepairs — ~150 lines apart. Removing one without the other silently turns the repair into dead code or reintroduces an orphan-window bug.assertionFailurecompiles out in release, so a missed early-access path ships as "works by accident" (an invisible, half-built UI) rather than a signal.windowManager/RootViewCoordinator.sharedbeing guarded by convention rather than by the type system.The early-access vector is
RootViewCoordinator.shared(astatic let) evaluatingWordPressAppDelegate.shared?.windowManagerat first access and caching it for the process lifetime — so the first touch ofRootViewCoordinator.shared/.sharedPresenteris what dragswindowManagerinto existence. Its ownwindowManagerproperty is alreadyWindowManager?, so the consumer already toleratesnil.Proposed direction
Single creation site + explicit optional. Make
windowManageran assignedWindowManager?, set when the window is built inshowInitialUI. Delete the lazy fallback and theexistingWindowadoption branch. Early access becomes anilcallers handle (composes withRootViewCoordinatoralready takingWindowManager?) instead of a fabricated scene-less window. Ripple is a couple of?at call sites.Remove the early-access vector (deeper). Stop
RootViewCoordinator.sharedfrom eagerly readingwindowManagerat static-init — resolve it lazily at point-of-use:Then nothing reaches
windowManagerbefore the scene exists, the fallback is provably dead and can be deleted outright, and the property could even go back to non-optional.Files
WordPress/Classes/System/WordPressAppDelegate.swift—windowManagerlazy getter,showInitialUI(in:)WordPress/Classes/System/Root View/RootViewCoordinator.swift—static let shared/sharedPresenterSurfaced during review of #25643. The PR description already notes that "the separation between the UI and the app process is still not very clean, but that can be tightened in future PRs" — this is part of that.