Skip to content

Commit 5eba934

Browse files
BohdanBohdan
authored andcommitted
Reduced re-render cycles
1 parent d4325f6 commit 5eba934

File tree

8 files changed

+104
-47
lines changed

8 files changed

+104
-47
lines changed

.claude/settings.local.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@
5959
"Read(//tmp/trace_extract/$uuid/**)",
6060
"Bash(xctrace help:*)",
6161
"Bash(xctrace export:*)",
62-
"Bash(python3:*)"
62+
"Bash(python3:*)",
63+
"WebSearch",
64+
"Bash(chmod +x /Users/lion/Projects/trace-analyzer/analyze_trace.py)",
65+
"Bash(grep -n \"orderOut\\\\|isVisible\\\\|alphaValue\\\\|LanguageIndicator\" /Users/lion/Projects/LanguageFlag/LanguageFlagUITests/*.swift)"
6366
]
6467
}
6568
}

LanguageFlag/Stories/LanguageViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class LanguageViewController: NSViewController {
2929
// MARK: - UI Components
3030
private let visualEffectView: NSVisualEffectView = {
3131
let view = NSVisualEffectView()
32-
view.blendingMode = .behindWindow
32+
view.blendingMode = .withinWindow
3333
view.material = .toolTip
3434
view.state = .active
3535
view.translatesAutoresizingMaskIntoConstraints = false

LanguageFlag/Stories/Preferences/Components/SliderTickLabels.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ struct SliderTickLabels: View {
2323
Text(label)
2424
.font(.system(size: 9))
2525
.foregroundColor(.secondary)
26-
.fixedSize()
2726
.position(x: trackInset + fraction * trackWidth, y: 7)
2827
}
2928
}

LanguageFlag/Stories/Preferences/Panes/AboutPreferencesPane.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,23 @@ struct AboutPreferencesPane: View {
1414

1515
@State private var isHovering = false
1616
@State private var rotationDegrees: Double = 0
17+
@State private var isVisible = false
1718

1819
// MARK: - Views
1920

2021
var body: some View {
2122
ZStack {
22-
animatedGradientBackground
23+
if isVisible {
24+
animatedGradientBackground
25+
}
2326

2427
ScrollView {
2528
appInfoSection
2629
.padding()
2730
}
2831
}
32+
.onAppear { isVisible = true }
33+
.onDisappear { isVisible = false }
2934
}
3035
}
3136

@@ -61,7 +66,9 @@ private extension AboutPreferencesPane {
6166
VStack(spacing: 16) {
6267
// App Icon with orbiting flags
6368
ZStack {
64-
orbitingFlagsView
69+
if isVisible {
70+
orbitingFlagsView
71+
}
6572

6673
interactiveAppIcon
6774
}

LanguageFlag/Stories/Preferences/Panes/AppearancePreferencesPane.swift

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ import SwiftUI
44
struct AppearancePreferencesPane: View {
55

66
// MARK: - Variables
7-
@ObservedObject private var preferences: UserPreferences
8-
9-
// MARK: - Init
10-
init(preferences: UserPreferences) {
11-
self.preferences = preferences
12-
}
7+
@Binding var opacity: Double
8+
@Binding var animationStyle: AnimationStyle
9+
@Binding var animationDuration: Double
10+
@Binding var displayDuration: Double
11+
@Binding var resetAnimationOnChange: Bool
1312

1413
// MARK: - Views
1514
var body: some View {
@@ -50,13 +49,13 @@ struct AppearancePreferencesPane: View {
5049

5150
HStack(alignment: .top, spacing: 8) {
5251
VStack(spacing: 2) {
53-
Slider(value: $preferences.opacity, in: 0.5...1.0, step: 0.05)
52+
Slider(value: $opacity, in: 0.5...1.0, step: 0.05)
5453
.accessibilityIdentifier("opacity_slider")
5554

5655
SliderTickLabels(labels: opacitySteps.map { String(format: "%.0f%%", $0 * 100) })
5756
}
5857

59-
Text(String(format: "%.0f%%", preferences.opacity * 100))
58+
Text(String(format: "%.0f%%", opacity * 100))
6059
.frame(width: 95, alignment: .trailing)
6160
.accessibilityIdentifier("opacity_value")
6261
}
@@ -101,7 +100,7 @@ struct AppearancePreferencesPane: View {
101100

102101
Spacer()
103102

104-
Toggle("Reset animation on layout change", isOn: $preferences.resetAnimationOnChange)
103+
Toggle("Reset animation on layout change", isOn: $resetAnimationOnChange)
105104
.toggleStyle(.switch)
106105
.labelsHidden()
107106
.accessibilityLabel("Reset animation on layout change")
@@ -119,13 +118,13 @@ struct AppearancePreferencesPane: View {
119118

120119
HStack(alignment: .top, spacing: 8) {
121120
VStack(spacing: 2) {
122-
Slider(value: $preferences.animationDuration, in: 0.1...1.0, step: 0.1)
121+
Slider(value: $animationDuration, in: 0.1...1.0, step: 0.1)
123122
.accessibilityIdentifier("animation_duration_slider")
124123

125124
SliderTickLabels(labels: animationSpeedSteps.map { String(format: "%.1f", $0) })
126125
}
127126

128-
Text(String(format: "%.1fs", preferences.animationDuration))
127+
Text(String(format: "%.1fs", animationDuration))
129128
.frame(width: 95, alignment: .trailing)
130129
.accessibilityIdentifier("animation_duration_value")
131130
}
@@ -145,12 +144,12 @@ struct AppearancePreferencesPane: View {
145144

146145
HStack(alignment: .top, spacing: 8) {
147146
VStack(spacing: 2) {
148-
Slider(value: $preferences.displayDuration, in: 0.5...5.0, step: 0.5)
147+
Slider(value: $displayDuration, in: 0.5...5.0, step: 0.5)
149148

150149
SliderTickLabels(labels: displayDurationSteps.map { String(format: "%.1f", $0) })
151150
}
152151

153-
Text(String(format: "%.1fs", preferences.displayDuration))
152+
Text(String(format: "%.1fs", displayDuration))
154153
.frame(width: 95, alignment: .trailing)
155154
}
156155

@@ -166,14 +165,14 @@ private extension AppearancePreferencesPane {
166165

167166
private func animationStyleButton(for style: AnimationStyle) -> some View {
168167
Button {
169-
preferences.animationStyle = style
168+
animationStyle = style
170169
} label: {
171170
Text(style.description)
172171
.font(.caption)
173172
.frame(maxWidth: .infinity)
174173
.padding(.vertical, 6)
175-
.background(preferences.animationStyle == style ? Color.accentColor : Color.gray.opacity(0.15))
176-
.foregroundColor(preferences.animationStyle == style ? .white : .primary)
174+
.background(animationStyle == style ? Color.accentColor : Color.gray.opacity(0.15))
175+
.foregroundColor(animationStyle == style ? .white : .primary)
177176
.cornerRadius(6)
178177
}
179178
.buttonStyle(.plain)

LanguageFlag/Stories/Preferences/Panes/GeneralPreferencesPane.swift

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import LaunchAtLogin
55
struct GeneralPreferencesPane: View {
66

77
// MARK: - Variables
8-
@ObservedObject private var preferences: UserPreferences
8+
@Binding var displayPosition: DisplayPosition
9+
@Binding var windowSize: WindowSize
10+
@Binding var showInMenuBar: Bool
11+
@Binding var showCapsLockIndicator: Bool
12+
@Binding var bypassClick: Bool
13+
let onReset: () -> Void
14+
915
@State private var showResetConfirmation = false
10-
11-
// MARK: - Init
12-
init(preferences: UserPreferences) {
13-
self.preferences = preferences
14-
}
1516

1617
// MARK: - Views
1718
var body: some View {
@@ -45,7 +46,7 @@ struct GeneralPreferencesPane: View {
4546
Text("Display Position")
4647
.font(.headline)
4748

48-
PositionPickerView(selectedPosition: $preferences.displayPosition)
49+
PositionPickerView(selectedPosition: $displayPosition)
4950
.frame(height: 160)
5051

5152
Text("Click a position to place the indicator on your screen")
@@ -63,8 +64,8 @@ struct GeneralPreferencesPane: View {
6364
VStack(spacing: 2) {
6465
Slider(
6566
value: Binding(
66-
get: { Double(WindowSize.allCases.firstIndex(of: preferences.windowSize) ?? 1) },
67-
set: { preferences.windowSize = WindowSize.allCases[Int($0)] }
67+
get: { Double(WindowSize.allCases.firstIndex(of: windowSize) ?? 1) },
68+
set: { windowSize = WindowSize.allCases[Int($0)] }
6869
),
6970
in: 0...3,
7071
step: 1
@@ -73,7 +74,7 @@ struct GeneralPreferencesPane: View {
7374
SliderTickLabels(labels: WindowSize.allCases.map(\.description))
7475
}
7576

76-
Text(preferences.windowSize.description)
77+
Text(windowSize.description)
7778
.frame(width: 95, alignment: .trailing)
7879
.foregroundColor(.primary)
7980
}
@@ -99,7 +100,7 @@ struct GeneralPreferencesPane: View {
99100
.foregroundColor(.secondary)
100101
}
101102
Spacer()
102-
Toggle("Show current layout in menu bar", isOn: $preferences.showInMenuBar)
103+
Toggle("Show current layout in menu bar", isOn: $showInMenuBar)
103104
.toggleStyle(.switch)
104105
.labelsHidden()
105106
.accessibilityIdentifier("menuBarToggle")
@@ -114,7 +115,7 @@ struct GeneralPreferencesPane: View {
114115
.foregroundColor(.secondary)
115116
}
116117
Spacer()
117-
Toggle("Show indicator on Caps Lock change", isOn: $preferences.showCapsLockIndicator)
118+
Toggle("Show indicator on Caps Lock change", isOn: $showCapsLockIndicator)
118119
.toggleStyle(.switch)
119120
.labelsHidden()
120121
.accessibilityIdentifier("capsLockToggle")
@@ -129,7 +130,7 @@ struct GeneralPreferencesPane: View {
129130
.foregroundColor(.secondary)
130131
}
131132
Spacer()
132-
Toggle("Bypass click", isOn: $preferences.bypassClick)
133+
Toggle("Bypass click", isOn: $bypassClick)
133134
.toggleStyle(.switch)
134135
.labelsHidden()
135136
.accessibilityIdentifier("bypassClickToggle")
@@ -178,7 +179,7 @@ struct GeneralPreferencesPane: View {
178179
titleVisibility: .visible
179180
) {
180181
Button("Reset", role: .destructive) {
181-
preferences.resetToDefaults()
182+
onReset()
182183
}
183184
}
184185
}

LanguageFlag/Stories/Preferences/Panes/ShortcutsPreferencesPane.swift

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@ import SwiftUI
44
struct ShortcutsPreferencesPane: View {
55

66
// MARK: - Variables
7-
@ObservedObject private var preferences: UserPreferences
8-
9-
// MARK: - Init
10-
init(preferences: UserPreferences) {
11-
self.preferences = preferences
12-
}
7+
@Binding var showShortcuts: Bool
138

149
// MARK: - Views
1510
var body: some View {
@@ -19,7 +14,7 @@ struct ShortcutsPreferencesPane: View {
1914
private var content: some View {
2015
ScrollView {
2116
VStack(alignment: .leading, spacing: 16) {
22-
Toggle("Show keyboard shortcuts for current layout", isOn: $preferences.showShortcuts)
17+
Toggle("Show keyboard shortcuts for current layout", isOn: $showShortcuts)
2318
.help("Display common keyboard shortcuts when switching layouts")
2419

2520
Divider()

LanguageFlag/Stories/Preferences/PreferencesView.swift

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,51 @@
11
import SwiftUI
22

3-
/// Main preferences view - tab-based navigation across preference panes
3+
// MARK: - State holder (re-renders on objectWillChange, but its body is trivial)
4+
5+
/// Owns the UserPreferences @StateObject and projects bindings into the static PreferencesView.
6+
/// Kept separate so that PreferencesView.body never re-runs due to UserPreferences changes.
47
struct PreferencesView: View {
58

6-
// MARK: - Variables
79
@StateObject private var preferences = UserPreferences.shared
810

11+
var body: some View {
12+
PreferencesContentView(
13+
displayPosition: $preferences.displayPosition,
14+
windowSize: $preferences.windowSize,
15+
showInMenuBar: $preferences.showInMenuBar,
16+
showCapsLockIndicator: $preferences.showCapsLockIndicator,
17+
bypassClick: $preferences.bypassClick,
18+
opacity: $preferences.opacity,
19+
animationStyle: $preferences.animationStyle,
20+
animationDuration: $preferences.animationDuration,
21+
displayDuration: $preferences.displayDuration,
22+
resetAnimationOnChange: $preferences.resetAnimationOnChange,
23+
showShortcuts: $preferences.showShortcuts,
24+
onReset: { UserPreferences.shared.resetToDefaults() }
25+
)
26+
}
27+
}
28+
29+
// MARK: - Static content (TabView — body only re-runs when its own @State/@Binding values change)
30+
31+
/// Main preferences view - tab-based navigation across preference panes.
32+
/// Takes all preferences as bindings so its body is not invalidated by unrelated property changes.
33+
private struct PreferencesContentView: View {
34+
35+
// MARK: - Bindings from UserPreferences
36+
@Binding var displayPosition: DisplayPosition
37+
@Binding var windowSize: WindowSize
38+
@Binding var showInMenuBar: Bool
39+
@Binding var showCapsLockIndicator: Bool
40+
@Binding var bypassClick: Bool
41+
@Binding var opacity: Double
42+
@Binding var animationStyle: AnimationStyle
43+
@Binding var animationDuration: Double
44+
@Binding var displayDuration: Double
45+
@Binding var resetAnimationOnChange: Bool
46+
@Binding var showShortcuts: Bool
47+
let onReset: () -> Void
48+
949
@State private var selectedPane: PreferencePane = .general
1050

1151
// MARK: - Views
@@ -27,14 +67,27 @@ struct PreferencesView: View {
2767
private func paneContent(for pane: PreferencePane) -> some View {
2868
switch pane {
2969
case .general:
30-
GeneralPreferencesPane(preferences: preferences)
70+
GeneralPreferencesPane(
71+
displayPosition: $displayPosition,
72+
windowSize: $windowSize,
73+
showInMenuBar: $showInMenuBar,
74+
showCapsLockIndicator: $showCapsLockIndicator,
75+
bypassClick: $bypassClick,
76+
onReset: onReset
77+
)
3178

3279
case .appearance:
33-
AppearancePreferencesPane(preferences: preferences)
80+
AppearancePreferencesPane(
81+
opacity: $opacity,
82+
animationStyle: $animationStyle,
83+
animationDuration: $animationDuration,
84+
displayDuration: $displayDuration,
85+
resetAnimationOnChange: $resetAnimationOnChange
86+
)
3487

3588
case .shortcuts:
3689
#if FEATURE_SHORTCUTS
37-
ShortcutsPreferencesPane(preferences: preferences)
90+
ShortcutsPreferencesPane(showShortcuts: $showShortcuts)
3891
#else
3992
EmptyView()
4093
#endif

0 commit comments

Comments
 (0)