Skip to content

Commit e335d1d

Browse files
fix: resolve SwiftUI view update crash in SettingsView Color Scheme picker
The crash was occurring because the `appearance.preference` @published property was being mutated directly by a Picker inside a ScrollView during SwiftUI's layout pass. Fixes: 1. Extracted Color Scheme settings into a dedicated `Appearance` tab to isolate it from the Engine tab's layout cycle. 2. Implemented a custom Binding in the Picker that defers the @published write using `Task { @mainactor in }`. This explicitly breaks out of the current view update pass before mutating the AppearanceStore.
1 parent cb4c6e4 commit e335d1d

1 file changed

Lines changed: 51 additions & 20 deletions

File tree

SwiftBuddy/SwiftBuddy/Views/SettingsView.swift

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,17 @@ struct SettingsView: View {
3535
enum SettingsTab: String, CaseIterable {
3636
case generation = "Generation"
3737
case engine = "Engine"
38+
case appearance = "Appearance"
3839
case console = "Console"
3940
case about = "About"
4041

4142
var icon: String {
4243
switch self {
43-
case .generation: return "slider.horizontal.3"
44-
case .engine: return "cpu"
45-
case .console: return "terminal"
46-
case .about: return "info.circle"
44+
case .generation: return "slider.horizontal.3"
45+
case .engine: return "cpu"
46+
case .appearance: return "paintpalette"
47+
case .console: return "terminal"
48+
case .about: return "info.circle"
4749
}
4850
}
4951
}
@@ -66,6 +68,8 @@ struct SettingsView: View {
6668
generationTab
6769
case .engine:
6870
engineTab
71+
case .appearance:
72+
appearanceTab
6973
case .console:
7074
consoleTab
7175
case .about:
@@ -501,22 +505,6 @@ struct SettingsView: View {
501505
)
502506
}
503507

504-
parameterCard("Appearance") {
505-
HStack {
506-
Label("Color Scheme", systemImage: "paintpalette")
507-
.foregroundStyle(SwiftBuddyTheme.textPrimary)
508-
.font(.callout)
509-
Spacer()
510-
}
511-
Picker("", selection: $appearance.preference) {
512-
HStack { Image(systemName: "moon.fill"); Text("Dark") }.tag("dark")
513-
HStack { Image(systemName: "sun.max.fill"); Text("Light") }.tag("light")
514-
HStack { Image(systemName: "circle.lefthalf.filled"); Text("System") }.tag("system")
515-
}
516-
.pickerStyle(.segmented)
517-
.tint(SwiftBuddyTheme.accent)
518-
}
519-
520508
parameterCard("Advanced Engine") {
521509
// ── TurboKV (per-request, no reload needed) ──────────────────────────
522510
toggleRow(
@@ -626,6 +614,49 @@ struct SettingsView: View {
626614
}
627615
}
628616

617+
// MARK: - Appearance Tab
618+
619+
// Use local state for the picker to avoid triggering a @Published write
620+
// directly from within a view update cycle, which causes the crash:
621+
// "Publishing changes from within view updates is not allowed"
622+
@State private var localColorScheme: String = "dark"
623+
624+
private var appearanceTab: some View {
625+
ScrollView {
626+
VStack(spacing: 16) {
627+
parameterCard("Theme") {
628+
HStack {
629+
Label("Color Scheme", systemImage: "paintpalette")
630+
.foregroundStyle(SwiftBuddyTheme.textPrimary)
631+
.font(.callout)
632+
Spacer()
633+
}
634+
Picker("", selection: Binding(
635+
get: { appearance.preference },
636+
set: { newValue in
637+
localColorScheme = newValue
638+
// Defer the @Published write to avoid the view update crash
639+
Task { @MainActor in
640+
appearance.preference = newValue
641+
}
642+
}
643+
)) {
644+
HStack { Image(systemName: "moon.fill"); Text("Dark") }.tag("dark")
645+
HStack { Image(systemName: "sun.max.fill"); Text("Light") }.tag("light")
646+
HStack { Image(systemName: "circle.lefthalf.filled"); Text("System") }.tag("system")
647+
}
648+
.pickerStyle(.segmented)
649+
.tint(SwiftBuddyTheme.accent)
650+
}
651+
}
652+
.padding(.horizontal, 16)
653+
.padding(.bottom, 24)
654+
}
655+
.onAppear {
656+
localColorScheme = appearance.preference
657+
}
658+
}
659+
629660
// MARK: — Console Tab
630661

631662
private var consoleTab: some View {

0 commit comments

Comments
 (0)