Skip to content

Harden reload against no-op orientation changes, modals, and lost windows - #46

Merged
daisuke0131 merged 1 commit into
masterfrom
fix/reload-hardening
Aug 9, 2026
Merged

Harden reload against no-op orientation changes, modals, and lost windows#46
daisuke0131 merged 1 commit into
masterfrom
fix/reload-hardening

Conversation

@daisuke0131

Copy link
Copy Markdown
Owner

Summary

Follow-ups to the 2.4.0 keep-measuring change, addressing findings from a code review of Sources/ViewMonitor:

  • No-op orientation changes no longer wipe measurement state: faceUp/faceDown/upsideDown fire orientationDidChange without rotating the interface; the deferred reload now skips when the key window and its bounds are unchanged, preserving the selection, distance reference, and InfoView
  • Deferred orientation reloads are coalesced: one physical rotation emits several notifications; only one reload is queued now instead of a full teardown/re-scan per notification
  • Modal presentations end measuring: keeping the shield across a programmatically presented alert/sheet covered the dialog and made its buttons untappable. The swizzled hook now passes isBeingPresented, and modal transitions cancel measuring (push transitions and rotation still keep it)
  • Keep-measuring is derived from launcherButton.isSelected instead of a stored flag, removing state duplication that could stay latched when a reload found no key window (invisible armed shield re-appearing on a later transition)
  • beginMeasuring(with:on:) extracted, deduplicating the show sequence between toggle-ON and the restore path
  • Launcher pinned top-right via autoresizing as insurance: even a reload racing the window resize cannot strand it off screen

Known limitations intentionally not addressed (niche / pre-existing): screens that fire viewDidAppear repeatedly on their own (e.g. auto-advancing page controllers) still re-scan per transition; iPad multi-scene key-window nondeterminism and transient key windows are pre-existing WindowProvider semantics.

Test plan

  • 130 unit tests / 20 suites green, incl. new lifecycle tests: modal presentation cancels measuring (shield never covers the dialog), non-modal transition keeps measuring — verified red-first by disabling the modal branch
  • 8 UI tests green (SwiftUI 6 + UIKit 2), incl. a new faceUp test asserting the selection and InfoView survive; verified to fail with the geometry guard removed
  • swiftlint --strict 0 violations; both example apps build

🤖 Generated with Claude Code

…t windows

Follow-ups to the keep-measuring change, from a code review of
Sources/ViewMonitor:

- Skip the deferred orientation reload when the key window and its
  bounds are unchanged (faceUp/faceDown/upsideDown): the interface does
  not rotate there, and reloading wiped the selection, distance
  reference, and InfoView while the screen looked identical. Covered by
  a faceUp UI test that fails with the guard removed.
- Coalesce deferred orientation reloads: one physical rotation emits
  several notifications, and each queued its own full teardown/re-scan.
- End measuring when a transition comes from a modal presentation
  (alerts, sheets): keeping the shield would cover the dialog and make
  its buttons untappable. Detected via isBeingPresented in the swizzled
  hook; push transitions and rotation still keep measuring. Covered by
  lifecycle unit tests through a new transition seam.
- Derive keep-measuring from launcherButton.isSelected instead of a
  stored flag. The flag could stay latched when a reload found no key
  window (armed shield with no visible UI re-appearing on a later
  transition) and could diverge from the button state in onToggle's
  guard path; derivation removes the state duplication entirely.
- Extract beginMeasuring(with:on:) shared by onToggle and the restore
  path, removing the copy-pasted show sequence.
- Pin the launcher to the top-right via autoresizing so that even a
  reload racing the window resize cannot strand it off screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@daisuke0131
daisuke0131 merged commit af2bee9 into master Aug 9, 2026
4 checks passed
@daisuke0131
daisuke0131 deleted the fix/reload-hardening branch August 9, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant