RexCode logo
← AI 隨筆
2026-06-23

LetPiano(Mac)測試時閃退:一個 SwiftUI 視窗布局的 crash

LetPiano (Mac) Crash During Testing: A SwiftUI Window-Layout Abort

在 Mac 上測試 LetPiano 的 archive build 時,跳出「LetPiano quit unexpectedly」。crash log 顯示是 SIGABRT:SwiftUI 在視窗布局的過程中又去改了約束,被 AppKit 擋下、丟出例外。簡單記錄這次的狀況。

SIGABRT SwiftUI AppKit · macOS log 解讀

📌 一句話結論

這是 Mac 版的本機測試 build(從 Xcode archive 跑的,不是 App Store 上架版)的閃退。log 指向一個 SwiftUI + AppKit 的視窗布局問題:當視窗「正在排版(layout)」的當下,畫面又觸發了一次會去改約束(constraints)的更新,AppKit 不允許這種「邊排版邊改」,於是丟出例外(NSException)、呼叫 abort() 強制結束。這是程式層的 bug,不是 Mac 壞掉;送審的 iOS(iPhone / iPad)版本不受這個影響

🖥 當時的情況

當時在 Mac(Mac13,1 / macOS 26.5.1)上跑 LetPiano 的 archive build 1.0.20260619 做上架前驗收。App 啟動後用了約 15 分鐘(log 顯示 23:15 啟動、23:30 閃退),畫面突然關閉,系統跳出「LetPiano quit unexpectedly」對話框。

macOS:LetPiano quit unexpectedly 對話框
macOS 的閃退對話框(Reopen / Ignore / Report)

🔍 這份 log 在說什麼

① 結束方式:SIGABRT(abort)

Exception Type: EXC_CRASH (SIGABRT)abort() called,發生在 主執行緒(main thread)。SIGABRT 通常代表「程式自己主動喊停」——多半是丟了一個沒接住的例外,或踩到系統的保護檢查。

② 關鍵的呼叫鏈

backtrace 由下往上講了一個很清楚的故事:

NSHostingView.windowDidLayout()(SwiftUI 收到「視窗剛排完版」)
invalidateProperties(SwiftUI 決定要更新一些屬性)
NSView setNeedsUpdateConstraints(去標記「約束要重算」)
_postWindowNeedsUpdateConstraints丟出 NSExceptionabort()

③ 白話翻譯

視窗正在「排版的那一拍」裡,SwiftUI 又反過來要求「重算約束」。對 AppKit 來說這是布局過程中的重入(re-entrancy)——等於施工到一半又改設計圖,它不接受,直接丟例外中止。

補充:log 路徑是 …/Xcode/Archives/…/LetPiano.app,代表這是本機 archive 測試 build,不是 App Store 下載的版本。app.rexcode.letpiano 三平台共用同一份 SwiftUI 程式碼,這個 crash 出現在 macOS 的 NSHostingView(把 SwiftUI 嵌進 AppKit 視窗的橋接層)。

🤔 大概是什麼引起的

這類 crash 在「SwiftUI 跑在 macOS 視窗裡」時並不少見,常見觸發點是:某個狀態變更(例如視窗縮放、onAppear、安全區/尺寸變化)在布局進行中就立刻改了會影響 layout 的東西,形成「排版 → 觸發更新 → 又要重新排版」的迴圈或重入。

單看這份 log 無法 100% 指到哪一行程式,但方向很明確:把「會改變 layout 的動作」從布局當下挪開(例如延後到下一個 runloop、或不要在計算畫面的當下就標記約束失效)。

🛠️ 該怎麼處理

✅ 三個方向

1. 當成 Mac 版的 layout bug 處理:它發生在 NSHostingView 的視窗布局橋接層,把「布局當下會改約束」的狀態變更延後(例如包進 DispatchQueue.main.async,或改用穩定的 layout 寫法),通常就能解。

2. 縮小範圍:從 backtrace 找出最近改動的、跟視窗尺寸/安全區/動畫有關的畫面,優先檢查在 onAppear 或 size 變化時是否同步改了狀態。

3. iOS 送審版照常:這是 Mac target 的偶發布局 crash,iPhone / iPad 版(已送審)走的是 iOS 的 hosting 路徑,不受這條 macOS 專屬的呼叫鏈影響。修好 Mac 後再隨更新一起出。

📄 Crash log(關鍵段落 · 已遮蔽)

以下是這次 crash 的關鍵段落(標頭 + 例外 backtrace)。為了隱私,已遮蔽 Crash Reporter KeyIncident Identifier。後面完整的 thread / image 清單與本次原因無關,已省略。

Process:        LetPiano [16257]
Identifier:     app.rexcode.letpiano
Version:        1.0.20260619 (1)
Hardware Model: Mac13,1
OS Version:     macOS 26.5.1 (25F80)
Crash Reporter Key:  ████████-████-████-████-████████████
Incident Identifier: ████████-████-████-████-████████████

Triggered by Thread: 0, Dispatch Queue: com.apple.main-thread
Exception Type:  EXC_CRASH (SIGABRT)
Termination Reason: Namespace SIGNAL, Code 6, Abort trap: 6
Application Specific Information: abort() called

Last Exception Backtrace:
0  CoreFoundation        __exceptionPreprocess + 164
1  libobjc.A.dylib       objc_exception_throw + 88
2  CoreFoundation        +[NSException exceptionWithName:reason:userInfo:]
3  AppKit                -[NSWindow(NSDisplayCycle) _postWindowNeedsUpdateConstraints] + 1716
4  AppKit                -[NSView _informContainerThatSubviewsNeedUpdateConstraints] + 64
5  AppKit                -[NSView setNeedsUpdateConstraints:] + 468
...
8  SwiftUICore           ViewGraphRootValueUpdater.invalidateProperties(_:mayDeferUpdate:)
10 SwiftUI               NSHostingView.invalidateSafeAreaCornerInsets()
25 SwiftUI               NSHostingView.windowDidLayout()
32 AppKit                -[NSWindow(NSConstraintBasedLayoutInternal) layoutIfNeeded] + 272
33 AppKit                __NSWindowGetDisplayCycleObserverForLayout_block_invoke
   ... (full thread / binary image list omitted) ...
分析方法:依 crash report 的 Exception TypeLast Exception Backtrace 判讀。這是 macOS archive 測試 build 的偶發布局 crash,非硬體故障,也不影響已送審的 iOS 版本。