fix: iOS 17 launch crash in the Lockdown gate presentation binding#2094
Conversation
The Lockdown fullScreenCover was driven by a computed Binding whose getter read the `lockdown` @EnvironmentObject and whose setter was a no-op. That presentation binding can never converge; on iOS 17's SwiftUI it re-enters the attribute graph until `_assertionFailure` fires during the first layout pass, crashing at launch. iOS 18+ tolerates it. The lockdown gate shipped in 2.7.16, so the crash is exclusive to that release. Drive the cover from a plain @State (`isShowingLockdownGate`) synced from the coordinator's blocking state via `.onChange` (plus an initial sync in `.onAppear`). LockdownSheet has no dismiss control and fullScreenCover has no interactive dismiss, so the gate remains non-dismissable — behavior is unchanged.
📝 WalkthroughWalkthrough
ChangesLockdown gate presentation
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
Meshtastic/Views/ContentView.swift (1)
60-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
initial: trueto avoid.onAppearflashes.In iOS 17+, you can pass
initial: trueto.onChangeso the closure evaluates and applies the state before the view's first render. This eliminates the need to manually synchronize the state in.onAppearand guarantees the view won't suffer from a single-frame flash where the cover isn't presented yet on launch.♻️ Proposed refactor
- // Present the gate if the device is already in a blocking state when - // this view appears. - isShowingLockdownGate = isLockdownGateActive } - .onChange(of: isLockdownGateActive) { _, active in + .onChange(of: isLockdownGateActive, initial: true) { _, active in // Follow the coordinator's blocking state. The gate never closes from // user interaction (fullScreenCover has no interactive dismiss and // LockdownSheet exposes no dismiss control), so this is the only path // that shows or hides it. isShowingLockdownGate = active }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Meshtastic/Views/ContentView.swift` around lines 60 - 70, Update the .onChange handler for isLockdownGateActive to use the iOS 17+ initial: true option, allowing it to synchronize isShowingLockdownGate before the first render. Remove the redundant .onAppear synchronization while preserving the existing active-state behavior and presentation logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@Meshtastic/Views/ContentView.swift`:
- Around line 60-70: Update the .onChange handler for isLockdownGateActive to
use the iOS 17+ initial: true option, allowing it to synchronize
isShowingLockdownGate before the first render. Remove the redundant .onAppear
synchronization while preserving the existing active-state behavior and
presentation logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 5bb5a4aa-0dee-469c-b50a-d76949bb416f
📒 Files selected for processing (1)
Meshtastic/Views/ContentView.swift
Summary
Fixes a launch-time
SIGTRAPthat is specific to iOS/iPadOS 17 (iOS 18+ is unaffected). It is the dominant crash for the 2.7.16 App Store release by a wide margin.Root cause
The Lockdown gate in
ContentViewwas presented with:A presentation
Bindingwhose getter reads observable state and whose setter is a no-op can never converge: SwiftUI's presentation machinery writes the binding, the no-op setter drops the write, and the getter still reflects the coordinator. On iOS 17's SwiftUI this re-enters the AttributeGraph until it trips_assertionFailureduring the first layout pass — so affected iOS 17 users crash at launch. iOS 18+'s presentation code tolerates the same construct.A symbolicated production crash report pins the crashed thread to
ContentView.body.getter→ the cover's binding closure →isLockdownGateActive.getter, recursing through AttributeGraph to_assertionFailure, exclusively on iOS/iPadOS 17.The Lockdown gate shipped in 2.7.16 (#1779), which is why the crash is exclusive to this release.
Fix
Drive the cover from a plain
@State(isShowingLockdownGate) synced from the coordinator's blocking state via.onChange(with an initial sync in.onAppear) — a normal, converging presentation binding.Behavior is unchanged:
LockdownSheetexposes no dismiss control andfullScreenCoverhas no interactive dismiss, so the gate stays non-dismissable and closes only when the coordinator leaves a blocking state — exactly as before.Testing
@Statebinding). The production crash did not reproduce on the 17.5 simulator — it is a real-device launch race (crashing samples are on iOS 17.7.x; simulator SwiftUI on 17.5 tolerates the binding).Risk
Low — one view, no behavior change to the gate, no API changes. Good candidate for a 2.7.17 hotfix given the iOS 17 launch impact.