Showing 10 changed files with 95 additions and 185 deletions
+56 -0
Documentation/Navigation Style Decisions.md
@@ -0,0 +1,56 @@
1
+# Navigation Style Decisions
2
+
3
+## Obiectiv
4
+
5
+Navigația aplicației folosește stilul SwiftUI implicit, cu un singur set de modificatori aplicat consistent în toate view-urile. Nu există logică condiționată per platformă pentru controlul navigației.
6
+
7
+## Deviații de la comportamentul SwiftUI implicit
8
+
9
+### 1. Titlu inline (`.navigationBarTitleDisplayMode(.inline)`)
10
+
11
+**Aplicat pe:** toate view-urile care au `.navigationTitle(...)`.
12
+
13
+**Motivație:** Implicit, SwiftUI afișează titlul mare (`.large`) în prima fereastră a unui NavigationStack sau NavigationView. Stilul large nu se potrivește layoutului aplicației, care folosește un tab bar custom imediat sub navigation bar — titlul mare consumă spațiu vertical fără beneficiu. Stilul inline plasează titlul centrat în toolbar, aliniind aspectul pe iPhone, iPad și Mac Catalyst.
14
+
15
+**Fișiere afectate:**
16
+- `Views/Meter/MeterView.swift` — live body și offline body
17
+- `Views/ChargedDevices/Details/ChargedDeviceDetailView.swift`
18
+- `Views/ChargedDevices/Sessions/ChargeSessionDetailView.swift`
19
+- `Views/ChargedDevices/Sessions/ChargedDeviceSessionsView.swift`
20
+- `Views/Meter/Tabs/Live/ChargerStandbyPowerWizardView.swift`
21
+- `Views/MeterMappingDebugView.swift`
22
+- `Views/DeviceHelpView.swift`
23
+- Sheets (deja aveau `.inline`): `ChargedDeviceEditorScaffoldView`, `SidebarChargedDeviceLibraryView`, și altele
24
+
25
+### 2. Font titlu navigation bar (19pt semibold)
26
+
27
+**Aplicat în:** `AppDelegate.configureNavigationBarAppearance()` via `UINavigationBarAppearance`.
28
+
29
+**Motivație:** Fontul implicit al titlului inline este `.headline` (17pt semibold), perceput ca prea mic față de densitatea vizuală a conținutului. 19pt semibold oferă mai multă prezență fără a afecta spațiul disponibil, întrucât titlul rămâne pe un singur rând.
30
+
31
+**Configurare:**
32
+```swift
33
+let titleFont = UIFont.systemFont(ofSize: 19, weight: .semibold)
34
+let appearance = UINavigationBarAppearance()
35
+appearance.configureWithDefaultBackground()
36
+appearance.titleTextAttributes = [.font: titleFont]
37
+UINavigationBar.appearance().standardAppearance = appearance
38
+UINavigationBar.appearance().scrollEdgeAppearance = appearance
39
+UINavigationBar.appearance().compactAppearance = appearance
40
+```
41
+
42
+## Ce NU s-a schimbat față de implicit
43
+
44
+- `NavigationView` cu `.navigationViewStyle(.stack)` pe iPhone și `.navigationViewStyle(.columns)` pe iPad/Mac — arhitectural, nu cosmetic
45
+- Toolbar items (`.toolbar { }`) — plasate standard pe `.navigationBarTrailing` / `.cancellationAction` / `.confirmationAction`
46
+- Fundalul navigation bar — `configureWithDefaultBackground()` păstrează comportamentul implicit al sistemului (translucid/blur)
47
+
48
+## Istoric
49
+
50
+Anterior existau mai multe straturi de modificatori conflictuali adăugați în tentative de a obține un layout compact "Nav Control – Title – Tools" pe Mac Catalyst și iPad:
51
+- `navigationBarHidden(landscape)` — ascundea bara în landscape pe Catalyst
52
+- `IOSOnlyNavBar` (ViewModifier) — aplica titlu și toolbar condițional pe `!isTrueMacApp`
53
+- `macNavigationHeader` și `offlineMacHeader` — headere custom inline în VStack care dublau controalele când bara de sistem era vizibilă
54
+- `ToolbarItemGroup(placement: .primaryAction) {}` gol pe Catalyst — crea artefacte vizuale
55
+
56
+Toate au fost eliminate în aprilie 2026. Soluția corectă a fost `.navigationBarTitleDisplayMode(.inline)` aplicat consistent.
+11 -0
USB Meter/AppDelegate.swift
@@ -136,9 +136,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
136 136
         UNUserNotificationCenter.current().delegate = self
137 137
         application.registerForRemoteNotifications()
138 138
         appData.activateChargeInsights(context: persistentContainer.viewContext)
139
+        configureNavigationBarAppearance()
139 140
         return true
140 141
     }
141 142
 
143
+    private func configureNavigationBarAppearance() {
144
+        let titleFont = UIFont.systemFont(ofSize: 19, weight: .semibold)
145
+        let appearance = UINavigationBarAppearance()
146
+        appearance.configureWithDefaultBackground()
147
+        appearance.titleTextAttributes = [.font: titleFont]
148
+        UINavigationBar.appearance().standardAppearance = appearance
149
+        UINavigationBar.appearance().scrollEdgeAppearance = appearance
150
+        UINavigationBar.appearance().compactAppearance = appearance
151
+    }
152
+
142 153
     private func logRuntimeICloudDiagnostics() {
143 154
         #if DEBUG
144 155
         let hasUbiquityIdentityToken = FileManager.default.ubiquityIdentityToken != nil
+0 -24
USB Meter/Extensions/View.swift
@@ -7,27 +7,3 @@
7 7
 //
8 8
 
9 9
 import SwiftUI
10
-
11
-/* MARK: Iusless...
12
-enum XNavigationViewStyle {
13
-    case auto
14
-    case doubleColumn
15
-    case stack
16
-}
17
-
18
-extension View {
19
-    func xNavigationViewStyle(_ style: XNavigationViewStyle) -> some View {
20
-        switch style {
21
-        case .auto:
22
-            track("auto")
23
-            return AnyView(self.navigationViewStyle(DefaultNavigationViewStyle()))
24
-        case .doubleColumn:
25
-            track("doubleColumn")
26
-            return AnyView(self.navigationViewStyle(DoubleColumnNavigationViewStyle()))
27
-        case .stack:
28
-            track("stack")
29
-            return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
30
-        }
31
-    }
32
-}
33
-*/
+2 -0
USB Meter/Views/ChargedDevices/Details/ChargedDeviceDetailView.swift
@@ -33,10 +33,12 @@ struct ChargedDeviceDetailView: View {
33 33
             if let chargedDevice = appData.chargedDeviceSummary(id: chargedDeviceID) {
34 34
                 tabbedDetailView(chargedDevice)
35 35
                 .navigationTitle(chargedDevice.name)
36
+                .navigationBarTitleDisplayMode(.inline)
36 37
             } else {
37 38
                 Text("This device is no longer available.")
38 39
                     .foregroundColor(.secondary)
39 40
                     .navigationTitle("Device")
41
+                    .navigationBarTitleDisplayMode(.inline)
40 42
             }
41 43
         }
42 44
         .sidebarToggleToolbarItem()
+2 -0
USB Meter/Views/ChargedDevices/Sessions/ChargeSessionDetailView.swift
@@ -233,6 +233,7 @@ struct ChargeSessionDetailView: View {
233 233
             .ignoresSafeArea()
234 234
         )
235 235
         .navigationTitle(session.status.isOpen ? "Current Session" : "Session Details")
236
+        .navigationBarTitleDisplayMode(.inline)
236 237
         .toolbar {
237 238
             ToolbarItemGroup(placement: .primaryAction) {
238 239
                 if session.status.isOpen == false {
@@ -262,6 +263,7 @@ struct ChargeSessionDetailView: View {
262 263
         .frame(maxWidth: .infinity, maxHeight: .infinity)
263 264
         .padding(24)
264 265
         .navigationTitle("Session")
266
+        .navigationBarTitleDisplayMode(.inline)
265 267
     }
266 268
 
267 269
     private func monitoringSessionCard(
+2 -0
USB Meter/Views/ChargedDevices/Sessions/ChargedDeviceSessionsView.swift
@@ -64,10 +64,12 @@ struct ChargedDeviceSessionsView: View {
64 64
                     .ignoresSafeArea()
65 65
                 )
66 66
                 .navigationTitle("Sessions")
67
+                .navigationBarTitleDisplayMode(.inline)
67 68
             } else {
68 69
                 Text("This device is no longer available.")
69 70
                     .foregroundColor(.secondary)
70 71
                     .navigationTitle("Sessions")
72
+                    .navigationBarTitleDisplayMode(.inline)
71 73
             }
72 74
         }
73 75
         .alert(item: $pendingSessionDeletion) { session in
+1 -0
USB Meter/Views/DeviceHelpView.swift
@@ -43,6 +43,7 @@ struct DeviceHelpView: View {
43 43
             .ignoresSafeArea()
44 44
         )
45 45
         .navigationTitle("Device Help")
46
+        .navigationBarTitleDisplayMode(.inline)
46 47
         .sidebarToggleToolbarItem()
47 48
     }
48 49
 
+17 -161
USB Meter/Views/Meter/MeterView.swift
@@ -106,13 +106,7 @@ struct MeterView: View {
106 106
     private static let isPhone: Bool = false
107 107
     #endif
108 108
 
109
-    // True only on Mac iPad App (Designed for iPad), false on Catalyst
110
-    private static let isTrueMacApp: Bool = ProcessInfo.processInfo.isiOSAppOnMac && !ProcessInfo.processInfo.isMacCatalystApp
111
-    
112 109
     @State private var selectedMeterTab: MeterTab = .home
113
-    @State private var navBarTitle: String = "Meter"
114
-    @State private var navBarShowRSSI: Bool = false
115
-    @State private var navBarRSSI: Int = 0
116 110
     @State private var landscapeTabBarHeight: CGFloat = 0
117 111
 
118 112
     // Offline mode state
@@ -151,10 +145,6 @@ struct MeterView: View {
151 145
             )
152 146
 
153 147
             VStack(spacing: 0) {
154
-                // Use custom header only on true Mac iPad App (Designed for iPad on Mac)
155
-                if Self.isTrueMacApp {
156
-                    macNavigationHeader
157
-                }
158 148
                 Group {
159 149
                     if landscape {
160 150
                         landscapeDeck(
@@ -173,34 +163,23 @@ struct MeterView: View {
173 163
                 }
174 164
                 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
175 165
             }
176
-            #if !targetEnvironment(macCatalyst)
177
-            .navigationBarHidden(Self.isTrueMacApp && landscape)
178
-            #else
179
-            .navigationBarHidden(landscape)
180
-            #endif
181 166
         }
182 167
         .background(meterBackground)
183
-        .modifier(IOSOnlyNavBar(
184
-            apply: !Self.isTrueMacApp,
185
-            title: navBarTitle,
186
-            showRSSI: navBarShowRSSI,
187
-            rssi: navBarRSSI,
188
-            meter: meter
189
-        ))
190
-        .onAppear {
191
-            navBarTitle = meter.name.isEmpty ? "Meter" : meter.name
192
-            navBarShowRSSI = meter.operationalState > .notPresent
193
-            navBarRSSI = meter.btSerial.averageRSSI
194
-        }
195
-        .onChange(of: meter.name) { name in
196
-            navBarTitle = name.isEmpty ? "Meter" : name
197
-        }
198
-        .onChange(of: meter.operationalState) { state in
199
-            navBarShowRSSI = state > .notPresent
200
-        }
201
-        .onChange(of: meter.btSerial.averageRSSI) { newRSSI in
202
-            if abs(newRSSI - navBarRSSI) >= 5 {
203
-                navBarRSSI = newRSSI
168
+        .navigationTitle(meter.name.isEmpty ? "Meter" : meter.name)
169
+        .navigationBarTitleDisplayMode(.inline)
170
+        .toolbar {
171
+            ToolbarItemGroup(placement: .navigationBarTrailing) {
172
+                MeterConnectionToolbarButton(
173
+                    operationalState: meter.operationalState,
174
+                    showsTitle: false,
175
+                    connectAction: { meter.connect() },
176
+                    disconnectAction: { meter.disconnect() }
177
+                )
178
+                .font(.body.weight(.semibold))
179
+                if meter.operationalState > .notPresent {
180
+                    RSSIView(RSSI: meter.btSerial.averageRSSI)
181
+                        .frame(width: 18, height: 18)
182
+                }
204 183
             }
205 184
         }
206 185
         .onChange(of: selectedMeterTab) { newTab in
@@ -208,55 +187,6 @@ struct MeterView: View {
208 187
         }
209 188
     }
210 189
 
211
-    // MARK: - Custom navigation header for Designed-for-iPad on Mac
212
-
213
-    private var macNavigationHeader: some View {
214
-        HStack(spacing: 12) {
215
-            Button {
216
-                dismiss()
217
-            } label: {
218
-                HStack(spacing: 4) {
219
-                    Image(systemName: "chevron.left")
220
-                        .font(.body.weight(.semibold))
221
-                    Text("USB Meters")
222
-                }
223
-                .foregroundColor(.accentColor)
224
-            }
225
-            .buttonStyle(.plain)
226
-
227
-            Text(meter.name.isEmpty ? "Meter" : meter.name)
228
-                .font(.headline)
229
-                .lineLimit(1)
230
-
231
-            Spacer()
232
-
233
-            MeterConnectionToolbarButton(
234
-                operationalState: meter.operationalState,
235
-                showsTitle: true,
236
-                connectAction: { meter.connect() },
237
-                disconnectAction: { meter.disconnect() }
238
-            )
239
-
240
-            if meter.operationalState > .notPresent {
241
-                RSSIView(RSSI: meter.btSerial.averageRSSI)
242
-                    .frame(width: 18, height: 18)
243
-            }
244
-
245
-        }
246
-        .padding(.horizontal, 16)
247
-        .padding(.vertical, 10)
248
-        .background(
249
-            Rectangle()
250
-                .fill(.ultraThinMaterial)
251
-                .ignoresSafeArea(edges: .top)
252
-        )
253
-        .overlay(alignment: .bottom) {
254
-            Rectangle()
255
-                .fill(Color.secondary.opacity(0.12))
256
-                .frame(height: 1)
257
-        }
258
-    }
259
-
260 190
     private func portraitContent(
261 191
         size: CGSize,
262 192
         tabBarStyle: TabBarStyle,
@@ -684,9 +614,6 @@ struct MeterView: View {
684 614
     @ViewBuilder
685 615
     private func offlineBody(summary: AppData.MeterSummary) -> some View {
686 616
         VStack(spacing: 0) {
687
-            if Self.isTrueMacApp {
688
-                offlineMacHeader(name: summary.displayName)
689
-            }
690 617
             offlineTabBar(tint: summary.tint)
691 618
             offlineTabContent(summary: summary)
692 619
                 .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
@@ -695,12 +622,8 @@ struct MeterView: View {
695 622
                 .animation(.easeInOut(duration: 0.22), value: selectedOfflineTab)
696 623
         }
697 624
         .background(offlineBackground(tint: summary.tint))
698
-        #if !targetEnvironment(macCatalyst)
699
-        .navigationBarHidden(Self.isTrueMacApp)
700
-        #else
701
-        .navigationBarHidden(false)
702
-        #endif
703
-        .navigationBarTitle(summary.displayName, displayMode: .inline)
625
+        .navigationTitle(summary.displayName)
626
+        .navigationBarTitleDisplayMode(.inline)
704 627
         .onAppear {
705 628
             offlineName = summary.displayName
706 629
             offlineTemperatureUnit = appData.temperatureUnitPreference(for: summary.macAddress)
@@ -873,34 +796,6 @@ struct MeterView: View {
873 796
         .meterCard(tint: tint, fillOpacity: 0.18, strokeOpacity: 0.24)
874 797
     }
875 798
 
876
-    private func offlineMacHeader(name: String) -> some View {
877
-        HStack(spacing: 12) {
878
-            Button { dismiss() } label: {
879
-                HStack(spacing: 4) {
880
-                    Image(systemName: "chevron.left")
881
-                        .font(.body.weight(.semibold))
882
-                    Text("USB Meters")
883
-                }
884
-                .foregroundColor(.accentColor)
885
-            }
886
-            .buttonStyle(.plain)
887
-            Text(name).font(.headline).lineLimit(1)
888
-            Spacer()
889
-        }
890
-        .padding(.horizontal, 16)
891
-        .padding(.vertical, 10)
892
-        .background(
893
-            Rectangle()
894
-                .fill(.ultraThinMaterial)
895
-                .ignoresSafeArea(edges: .top)
896
-        )
897
-        .overlay(alignment: .bottom) {
898
-            Rectangle()
899
-                .fill(Color.secondary.opacity(0.12))
900
-                .frame(height: 1)
901
-        }
902
-    }
903
-
904 799
     private func offlineStatusHeader(summary: AppData.MeterSummary) -> some View {
905 800
         HStack(spacing: 12) {
906 801
             Image(systemName: "sensor.tag.radiowaves.forward.fill")
@@ -954,42 +849,3 @@ private struct MeterTabBarHeightPreferenceKey: PreferenceKey {
954 849
     }
955 850
 }
956 851
 
957
-// MARK: - Conditional navigation bar modifier (skipped on Designed-for-iPad / Mac)
958
-
959
-private struct IOSOnlyNavBar: ViewModifier {
960
-    let apply: Bool
961
-    let title: String
962
-    let showRSSI: Bool
963
-    let rssi: Int
964
-    let meter: Meter
965
-
966
-    @ViewBuilder
967
-    func body(content: Content) -> some View {
968
-        if apply {
969
-            content
970
-                .navigationBarTitle(title, displayMode: .inline)
971
-                .toolbar {
972
-                    ToolbarItemGroup(placement: .navigationBarTrailing) {
973
-                        MeterConnectionToolbarButton(
974
-                            operationalState: meter.operationalState,
975
-                            showsTitle: false,
976
-                            connectAction: { meter.connect() },
977
-                            disconnectAction: { meter.disconnect() }
978
-                        )
979
-                        .font(.body.weight(.semibold))
980
-                        if showRSSI {
981
-                            RSSIView(RSSI: rssi)
982
-                                .frame(width: 18, height: 18)
983
-                        }
984
-                    }
985
-                }
986
-                #if targetEnvironment(macCatalyst)
987
-                .toolbar {
988
-                    ToolbarItemGroup(placement: .primaryAction) {}
989
-                }
990
-                #endif
991
-        } else {
992
-            content
993
-        }
994
-    }
995
-}
+3 -0
USB Meter/Views/Meter/Tabs/Live/ChargerStandbyPowerWizardView.swift
@@ -53,6 +53,7 @@ struct ChargerStandbyPowerWizardView: View {
53 53
             .ignoresSafeArea()
54 54
         )
55 55
         .navigationTitle(navigationTitleText)
56
+        .navigationBarTitleDisplayMode(.inline)
56 57
         .sheet(isPresented: $chargerLibraryVisibility) {
57 58
             ChargedDeviceLibrarySheetView(
58 59
                 meterMACAddress: selectedMeterSummary?.macAddress ?? "",
@@ -699,6 +700,7 @@ struct ChargerStandbyPowerMeasurementsView: View {
699 700
                 Text("This charger is no longer available.")
700 701
                     .foregroundColor(.secondary)
701 702
                     .navigationTitle("Saved Measurements")
703
+                    .navigationBarTitleDisplayMode(.inline)
702 704
             }
703 705
         }
704 706
     }
@@ -752,6 +754,7 @@ struct ChargerStandbyPowerMeasurementsView: View {
752 754
         }
753 755
         .environment(\.editMode, $editMode)
754 756
         .navigationTitle("Saved Measurements")
757
+        .navigationBarTitleDisplayMode(.inline)
755 758
         .toolbar {
756 759
             ToolbarItem(placement: .primaryAction) {
757 760
                 Button(editMode.isEditing ? "Done" : "Select") {
+1 -0
USB Meter/Views/MeterMappingDebugView.swift
@@ -53,6 +53,7 @@ struct MeterMappingDebugView: View {
53 53
         }
54 54
         .listStyle(.insetGrouped)
55 55
         .navigationTitle("Meter Sync Debug")
56
+        .navigationBarTitleDisplayMode(.inline)
56 57
         .onAppear(perform: reload)
57 58
         .onReceive(changePublisher) { _ in reload() }
58 59
         .toolbar {