Newer Older
628 lines | 21.824kb
Bogdan Timofte authored 2 months ago
1
//
2
//  MeterView.swift
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 04/03/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8
// MARK: Parent frame https://stackoverflow.com/questions/56832865/how-to-access-parents-frame-in-swiftui
9

            
10
import SwiftUI
11
import CoreBluetooth
12

            
13
struct MeterView: View {
Bogdan Timofte authored 2 months ago
14
    private struct TabBarStyle {
15
        let showsTitles: Bool
16
        let horizontalPadding: CGFloat
17
        let topPadding: CGFloat
18
        let bottomPadding: CGFloat
19
        let chipHorizontalPadding: CGFloat
20
        let chipVerticalPadding: CGFloat
21
        let outerPadding: CGFloat
22
        let maxWidth: CGFloat
23
        let barBackgroundOpacity: CGFloat
24
        let materialOpacity: CGFloat
25
        let shadowOpacity: CGFloat
26
        let floatingInset: CGFloat
27

            
28
        static let portrait = TabBarStyle(
29
            showsTitles: true,
30
            horizontalPadding: 16,
31
            topPadding: 10,
32
            bottomPadding: 8,
33
            chipHorizontalPadding: 10,
34
            chipVerticalPadding: 7,
35
            outerPadding: 6,
36
            maxWidth: 420,
37
            barBackgroundOpacity: 0.10,
38
            materialOpacity: 0.78,
39
            shadowOpacity: 0,
40
            floatingInset: 0
41
        )
42

            
Bogdan Timofte authored 2 months ago
43
        static let portraitCompact = TabBarStyle(
44
            showsTitles: false,
45
            horizontalPadding: 16,
46
            topPadding: 10,
47
            bottomPadding: 8,
48
            chipHorizontalPadding: 12,
49
            chipVerticalPadding: 10,
50
            outerPadding: 6,
51
            maxWidth: 320,
52
            barBackgroundOpacity: 0.14,
53
            materialOpacity: 0.90,
54
            shadowOpacity: 0,
55
            floatingInset: 0
56
        )
57

            
Bogdan Timofte authored 2 months ago
58
        static let landscapeInline = TabBarStyle(
59
            showsTitles: true,
60
            horizontalPadding: 12,
61
            topPadding: 10,
62
            bottomPadding: 8,
63
            chipHorizontalPadding: 10,
64
            chipVerticalPadding: 7,
65
            outerPadding: 6,
66
            maxWidth: 420,
67
            barBackgroundOpacity: 0.10,
68
            materialOpacity: 0.78,
69
            shadowOpacity: 0,
70
            floatingInset: 0
71
        )
72

            
73
        static let landscapeFloating = TabBarStyle(
74
            showsTitles: false,
75
            horizontalPadding: 16,
76
            topPadding: 10,
77
            bottomPadding: 0,
78
            chipHorizontalPadding: 11,
79
            chipVerticalPadding: 11,
80
            outerPadding: 7,
81
            maxWidth: 260,
Bogdan Timofte authored 2 months ago
82
            barBackgroundOpacity: 0.16,
83
            materialOpacity: 0.88,
84
            shadowOpacity: 0.12,
Bogdan Timofte authored 2 months ago
85
            floatingInset: 12
86
        )
87
    }
88

            
Bogdan Timofte authored 2 months ago
89
    private enum MeterTab: String, Hashable {
Bogdan Timofte authored 2 months ago
90
        case home
Bogdan Timofte authored 2 months ago
91
        case live
92
        case chart
Bogdan Timofte authored a month ago
93
        case chargeRecord
94
        case dataGroups
Bogdan Timofte authored 2 months ago
95
        case settings
Bogdan Timofte authored 2 months ago
96

            
97
        var systemImage: String {
98
            switch self {
Bogdan Timofte authored 2 months ago
99
            case .home: return "house.fill"
Bogdan Timofte authored 2 months ago
100
            case .live: return "waveform.path.ecg"
101
            case .chart: return "chart.xyaxis.line"
Bogdan Timofte authored a month ago
102
            case .chargeRecord: return "gauge.with.dots.needle.50percent"
103
            case .dataGroups: return "square.grid.2x2.fill"
Bogdan Timofte authored 2 months ago
104
            case .settings: return "gearshape.fill"
Bogdan Timofte authored 2 months ago
105
            }
106
        }
107
    }
Bogdan Timofte authored 2 months ago
108

            
109
    @EnvironmentObject private var meter: Meter
Bogdan Timofte authored 2 months ago
110
    @Environment(\.dismiss) private var dismiss
111

            
112
    private static let isMacIPadApp: Bool = ProcessInfo.processInfo.isiOSAppOnMac
Bogdan Timofte authored 2 months ago
113
    #if os(iOS)
114
    private static let isPhone: Bool = UIDevice.current.userInterfaceIdiom == .phone
115
    #else
116
    private static let isPhone: Bool = false
117
    #endif
Bogdan Timofte authored 2 months ago
118

            
Bogdan Timofte authored 2 months ago
119
    @State private var selectedMeterTab: MeterTab = .home
Bogdan Timofte authored 2 months ago
120
    @State private var navBarTitle: String = "Meter"
121
    @State private var navBarShowRSSI: Bool = false
122
    @State private var navBarRSSI: Int = 0
Bogdan Timofte authored 2 months ago
123
    @State private var landscapeTabBarHeight: CGFloat = 0
Bogdan Timofte authored 2 months ago
124

            
Bogdan Timofte authored 2 months ago
125
    var body: some View {
Bogdan Timofte authored 2 months ago
126
        GeometryReader { proxy in
127
            let landscape = isLandscape(size: proxy.size)
Bogdan Timofte authored 2 months ago
128
            let usesOverlayTabBar = landscape && Self.isPhone
Bogdan Timofte authored 2 months ago
129
            let tabBarStyle = tabBarStyle(
130
                for: landscape,
131
                usesOverlayTabBar: usesOverlayTabBar,
132
                size: proxy.size
133
            )
Bogdan Timofte authored 2 months ago
134

            
Bogdan Timofte authored 2 months ago
135
            VStack(spacing: 0) {
136
                if Self.isMacIPadApp {
137
                    macNavigationHeader
138
                }
139
                Group {
140
                    if landscape {
Bogdan Timofte authored 2 months ago
141
                        landscapeDeck(
142
                            size: proxy.size,
143
                            usesOverlayTabBar: usesOverlayTabBar,
144
                            tabBarStyle: tabBarStyle
145
                        )
Bogdan Timofte authored 2 months ago
146
                    } else {
Bogdan Timofte authored 2 months ago
147
                        portraitContent(size: proxy.size, tabBarStyle: tabBarStyle)
Bogdan Timofte authored 2 months ago
148
                    }
Bogdan Timofte authored 2 months ago
149
                }
Bogdan Timofte authored 2 months ago
150
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
Bogdan Timofte authored 2 months ago
151
            }
Bogdan Timofte authored 2 months ago
152
            #if !targetEnvironment(macCatalyst)
Bogdan Timofte authored 2 months ago
153
            .navigationBarHidden(Self.isMacIPadApp || landscape)
Bogdan Timofte authored 2 months ago
154
            #endif
Bogdan Timofte authored 2 months ago
155
        }
156
        .background(meterBackground)
Bogdan Timofte authored 2 months ago
157
        .modifier(IOSOnlyNavBar(
158
            apply: !Self.isMacIPadApp,
159
            title: navBarTitle,
160
            showRSSI: navBarShowRSSI,
161
            rssi: navBarRSSI,
162
            meter: meter
163
        ))
164
        .onAppear {
165
            navBarTitle = meter.name.isEmpty ? "Meter" : meter.name
166
            navBarShowRSSI = meter.operationalState > .notPresent
167
            navBarRSSI = meter.btSerial.averageRSSI
168
        }
169
        .onChange(of: meter.name) { name in
170
            navBarTitle = name.isEmpty ? "Meter" : name
171
        }
172
        .onChange(of: meter.operationalState) { state in
173
            navBarShowRSSI = state > .notPresent
174
        }
175
        .onChange(of: meter.btSerial.averageRSSI) { newRSSI in
176
            if abs(newRSSI - navBarRSSI) >= 5 {
177
                navBarRSSI = newRSSI
178
            }
179
        }
Bogdan Timofte authored 2 months ago
180
        .onChange(of: selectedMeterTab) { newTab in
181
            meter.preferredTabIdentifier = newTab.rawValue
182
        }
Bogdan Timofte authored 2 months ago
183
    }
184

            
185
    // MARK: - Custom navigation header for Designed-for-iPad on Mac
186

            
187
    private var macNavigationHeader: some View {
188
        HStack(spacing: 12) {
189
            Button {
190
                dismiss()
191
            } label: {
192
                HStack(spacing: 4) {
193
                    Image(systemName: "chevron.left")
194
                        .font(.body.weight(.semibold))
195
                    Text("USB Meters")
196
                }
197
                .foregroundColor(.accentColor)
198
            }
199
            .buttonStyle(.plain)
200

            
201
            Text(meter.name.isEmpty ? "Meter" : meter.name)
202
                .font(.headline)
203
                .lineLimit(1)
204

            
205
            Spacer()
206

            
Bogdan Timofte authored 2 months ago
207
            MeterConnectionToolbarButton(
208
                operationalState: meter.operationalState,
209
                showsTitle: true,
210
                connectAction: { meter.connect() },
211
                disconnectAction: { meter.disconnect() }
212
            )
213

            
Bogdan Timofte authored 2 months ago
214
            if meter.operationalState > .notPresent {
Bogdan Timofte authored 2 months ago
215
                RSSIView(RSSI: meter.btSerial.averageRSSI)
Bogdan Timofte authored 2 months ago
216
                    .frame(width: 18, height: 18)
217
            }
Bogdan Timofte authored 2 months ago
218

            
219
        }
220
        .padding(.horizontal, 16)
221
        .padding(.vertical, 10)
222
        .background(
223
            Rectangle()
224
                .fill(.ultraThinMaterial)
225
                .ignoresSafeArea(edges: .top)
226
        )
227
        .overlay(alignment: .bottom) {
228
            Rectangle()
229
                .fill(Color.secondary.opacity(0.12))
230
                .frame(height: 1)
231
        }
Bogdan Timofte authored 2 months ago
232
    }
233

            
Bogdan Timofte authored 2 months ago
234
    private func portraitContent(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
235
        portraitSegmentedDeck(size: size, tabBarStyle: tabBarStyle)
236
    }
237

            
238
    @ViewBuilder
239
    private func landscapeDeck(size: CGSize, usesOverlayTabBar: Bool, tabBarStyle: TabBarStyle) -> some View {
240
        if usesOverlayTabBar {
241
            landscapeOverlaySegmentedDeck(size: size, tabBarStyle: tabBarStyle)
242
        } else {
243
            landscapeSegmentedDeck(size: size, tabBarStyle: tabBarStyle)
244
        }
Bogdan Timofte authored 2 months ago
245
    }
246

            
Bogdan Timofte authored 2 months ago
247
    private func landscapeOverlaySegmentedDeck(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
248
        ZStack(alignment: .top) {
249
            landscapeSegmentedContent(size: size)
250
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
251
                .padding(.top, landscapeContentTopPadding(for: tabBarStyle))
Bogdan Timofte authored 2 months ago
252
                .id(displayedMeterTab)
Bogdan Timofte authored 2 months ago
253
                .transition(.opacity.combined(with: .move(edge: .trailing)))
254

            
255
            segmentedTabBar(style: tabBarStyle, showsConnectionAction: !Self.isMacIPadApp)
256
        }
Bogdan Timofte authored 2 months ago
257
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored 2 months ago
258
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
259
        .onAppear {
Bogdan Timofte authored 2 months ago
260
            restoreSelectedTab()
Bogdan Timofte authored 2 months ago
261
        }
262
        .onPreferenceChange(MeterTabBarHeightPreferenceKey.self) { height in
263
            if height > 0 {
264
                landscapeTabBarHeight = height
265
            }
266
        }
Bogdan Timofte authored 2 months ago
267
    }
268

            
Bogdan Timofte authored 2 months ago
269
    private func landscapeSegmentedDeck(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
Bogdan Timofte authored 2 months ago
270
        VStack(spacing: 0) {
Bogdan Timofte authored 2 months ago
271
            segmentedTabBar(style: tabBarStyle, showsConnectionAction: !Self.isMacIPadApp)
Bogdan Timofte authored 2 months ago
272

            
273
            landscapeSegmentedContent(size: size)
274
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Bogdan Timofte authored 2 months ago
275
                .id(displayedMeterTab)
Bogdan Timofte authored 2 months ago
276
                .transition(.opacity.combined(with: .move(edge: .trailing)))
277
        }
Bogdan Timofte authored 2 months ago
278
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored 2 months ago
279
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
280
        .onAppear {
Bogdan Timofte authored 2 months ago
281
            restoreSelectedTab()
Bogdan Timofte authored 2 months ago
282
        }
283
    }
284

            
Bogdan Timofte authored 2 months ago
285
    private func portraitSegmentedDeck(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
Bogdan Timofte authored 2 months ago
286
        VStack(spacing: 0) {
Bogdan Timofte authored 2 months ago
287
            segmentedTabBar(style: tabBarStyle)
Bogdan Timofte authored 2 months ago
288

            
289
            portraitSegmentedContent(size: size)
290
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Bogdan Timofte authored 2 months ago
291
                .id(displayedMeterTab)
Bogdan Timofte authored 2 months ago
292
                .transition(.opacity.combined(with: .move(edge: .trailing)))
293
        }
Bogdan Timofte authored 2 months ago
294
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored 2 months ago
295
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
296
        .onAppear {
Bogdan Timofte authored 2 months ago
297
            restoreSelectedTab()
Bogdan Timofte authored 2 months ago
298
        }
299
    }
300

            
Bogdan Timofte authored 2 months ago
301
    private func segmentedTabBar(style: TabBarStyle, showsConnectionAction: Bool = false) -> some View {
302
        let isFloating = style.floatingInset > 0
303
        let cornerRadius = style.showsTitles ? 14.0 : 22.0
Bogdan Timofte authored 2 months ago
304
        let unselectedForegroundColor = isFloating ? Color.primary.opacity(0.86) : Color.primary
305
        let unselectedChipFill = isFloating ? Color.black.opacity(0.08) : Color.secondary.opacity(0.12)
Bogdan Timofte authored 2 months ago
306

            
307
        return HStack {
Bogdan Timofte authored 2 months ago
308
            Spacer(minLength: 0)
309

            
310
            HStack(spacing: 8) {
311
                ForEach(availableMeterTabs, id: \.self) { tab in
Bogdan Timofte authored 2 months ago
312
                    let isSelected = displayedMeterTab == tab
Bogdan Timofte authored 2 months ago
313

            
314
                    Button {
315
                        withAnimation(.easeInOut(duration: 0.2)) {
316
                            selectedMeterTab = tab
317
                        }
318
                    } label: {
319
                        HStack(spacing: 6) {
320
                            Image(systemName: tab.systemImage)
321
                                .font(.subheadline.weight(.semibold))
Bogdan Timofte authored 2 months ago
322
                            if style.showsTitles {
Bogdan Timofte authored a month ago
323
                                Text(title(for: tab))
Bogdan Timofte authored 2 months ago
324
                                    .font(.subheadline.weight(.semibold))
325
                                    .lineLimit(1)
326
                            }
Bogdan Timofte authored 2 months ago
327
                        }
Bogdan Timofte authored 2 months ago
328
                        .foregroundColor(
329
                            isSelected
330
                            ? .white
Bogdan Timofte authored 2 months ago
331
                            : unselectedForegroundColor
Bogdan Timofte authored 2 months ago
332
                        )
333
                        .padding(.horizontal, style.chipHorizontalPadding)
334
                        .padding(.vertical, style.chipVerticalPadding)
Bogdan Timofte authored 2 months ago
335
                        .frame(maxWidth: .infinity)
336
                        .background(
337
                            Capsule()
Bogdan Timofte authored 2 months ago
338
                                .fill(
339
                                    isSelected
340
                                    ? meter.color.opacity(isFloating ? 0.94 : 1)
Bogdan Timofte authored 2 months ago
341
                                    : unselectedChipFill
Bogdan Timofte authored 2 months ago
342
                                )
Bogdan Timofte authored 2 months ago
343
                        )
Bogdan Timofte authored 2 months ago
344
                    }
Bogdan Timofte authored 2 months ago
345
                    .buttonStyle(.plain)
Bogdan Timofte authored a month ago
346
                    .accessibilityLabel(title(for: tab))
Bogdan Timofte authored 2 months ago
347
                }
348
            }
Bogdan Timofte authored 2 months ago
349
            .frame(maxWidth: style.maxWidth)
350
            .padding(style.outerPadding)
Bogdan Timofte authored 2 months ago
351
            .background(
Bogdan Timofte authored 2 months ago
352
                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
353
                    .fill(
354
                        isFloating
355
                        ? LinearGradient(
356
                            colors: [
Bogdan Timofte authored 2 months ago
357
                                Color.white.opacity(0.76),
358
                                Color.white.opacity(0.52)
Bogdan Timofte authored 2 months ago
359
                            ],
360
                            startPoint: .topLeading,
361
                            endPoint: .bottomTrailing
362
                        )
363
                        : LinearGradient(
364
                            colors: [
365
                                Color.secondary.opacity(style.barBackgroundOpacity),
366
                                Color.secondary.opacity(style.barBackgroundOpacity)
367
                            ],
368
                            startPoint: .topLeading,
369
                            endPoint: .bottomTrailing
370
                        )
371
                    )
Bogdan Timofte authored 2 months ago
372
            )
Bogdan Timofte authored 2 months ago
373
            .overlay {
374
                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
375
                    .stroke(
Bogdan Timofte authored 2 months ago
376
                        isFloating ? Color.black.opacity(0.08) : Color.clear,
Bogdan Timofte authored 2 months ago
377
                        lineWidth: 1
378
                    )
379
            }
380
            .background {
Bogdan Timofte authored 2 months ago
381
                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
382
                    .fill(.ultraThinMaterial)
383
                    .opacity(style.materialOpacity)
Bogdan Timofte authored 2 months ago
384
            }
385
            .shadow(color: Color.black.opacity(style.shadowOpacity), radius: isFloating ? 28 : 24, x: 0, y: isFloating ? 16 : 12)
Bogdan Timofte authored 2 months ago
386

            
387
            Spacer(minLength: 0)
388
        }
Bogdan Timofte authored 2 months ago
389
        .padding(.horizontal, style.horizontalPadding)
390
        .padding(.top, style.topPadding)
391
        .padding(.bottom, style.bottomPadding)
Bogdan Timofte authored 2 months ago
392
        .background(
Bogdan Timofte authored 2 months ago
393
            GeometryReader { geometry in
394
                Color.clear
395
                    .preference(key: MeterTabBarHeightPreferenceKey.self, value: geometry.size.height)
396
            }
Bogdan Timofte authored 2 months ago
397
        )
Bogdan Timofte authored 2 months ago
398
        .padding(.horizontal, style.floatingInset)
399
        .background {
400
            if style.floatingInset == 0 {
401
                Rectangle()
402
                    .fill(.ultraThinMaterial)
403
                    .opacity(style.materialOpacity)
404
                    .ignoresSafeArea(edges: .top)
405
            }
406
        }
Bogdan Timofte authored 2 months ago
407
        .overlay(alignment: .bottom) {
Bogdan Timofte authored 2 months ago
408
            if style.floatingInset == 0 {
409
                Rectangle()
410
                    .fill(Color.secondary.opacity(0.12))
411
                    .frame(height: 1)
412
            }
413
        }
414
        .overlay(alignment: .trailing) {
415
            if showsConnectionAction {
416
                MeterConnectionToolbarButton(
417
                    operationalState: meter.operationalState,
418
                    showsTitle: false,
419
                    connectAction: { meter.connect() },
420
                    disconnectAction: { meter.disconnect() }
421
                )
422
                .font(.title3.weight(.semibold))
423
                .padding(.trailing, style.horizontalPadding + style.floatingInset + 4)
424
                .padding(.top, style.topPadding)
425
                .padding(.bottom, style.bottomPadding)
426
            }
Bogdan Timofte authored 2 months ago
427
        }
Bogdan Timofte authored 2 months ago
428
    }
429

            
Bogdan Timofte authored 2 months ago
430
    @ViewBuilder
431
    private func landscapeSegmentedContent(size: CGSize) -> some View {
Bogdan Timofte authored 2 months ago
432
        switch displayedMeterTab {
Bogdan Timofte authored 2 months ago
433
        case .home:
Bogdan Timofte authored a month ago
434
            MeterHomeTabView(
435
                size: size,
436
                isLandscape: true,
437
                showChargeRecordTab: {
438
                    withAnimation(.easeInOut(duration: 0.22)) {
439
                        selectedMeterTab = .chargeRecord
440
                    }
441
                },
442
                showDataGroupsTab: {
443
                    withAnimation(.easeInOut(duration: 0.22)) {
444
                        selectedMeterTab = .dataGroups
445
                    }
446
                }
447
            )
Bogdan Timofte authored 2 months ago
448
        case .live:
Bogdan Timofte authored 2 months ago
449
            MeterLiveTabView(size: size, isLandscape: true)
Bogdan Timofte authored 2 months ago
450
        case .chart:
Bogdan Timofte authored 2 months ago
451
            MeterChartTabView(size: size, isLandscape: true)
Bogdan Timofte authored a month ago
452
        case .chargeRecord:
Bogdan Timofte authored a month ago
453
            MeterChargeRecordTabView().equatable()
Bogdan Timofte authored a month ago
454
        case .dataGroups:
455
            MeterDataGroupsTabView()
Bogdan Timofte authored 2 months ago
456
        case .settings:
Bogdan Timofte authored 2 months ago
457
            MeterSettingsTabView(isMacIPadApp: Self.isMacIPadApp) {
458
                withAnimation(.easeInOut(duration: 0.22)) {
Bogdan Timofte authored 2 months ago
459
                    selectedMeterTab = .home
Bogdan Timofte authored 2 months ago
460
                }
461
            }
Bogdan Timofte authored 2 months ago
462
        }
463
    }
Bogdan Timofte authored 2 months ago
464

            
Bogdan Timofte authored 2 months ago
465
    @ViewBuilder
466
    private func portraitSegmentedContent(size: CGSize) -> some View {
Bogdan Timofte authored 2 months ago
467
        switch displayedMeterTab {
Bogdan Timofte authored 2 months ago
468
        case .home:
Bogdan Timofte authored a month ago
469
            MeterHomeTabView(
470
                size: size,
471
                isLandscape: false,
472
                showChargeRecordTab: {
473
                    withAnimation(.easeInOut(duration: 0.22)) {
474
                        selectedMeterTab = .chargeRecord
475
                    }
476
                },
477
                showDataGroupsTab: {
478
                    withAnimation(.easeInOut(duration: 0.22)) {
479
                        selectedMeterTab = .dataGroups
480
                    }
481
                }
482
            )
Bogdan Timofte authored 2 months ago
483
        case .live:
Bogdan Timofte authored 2 months ago
484
            MeterLiveTabView(size: size, isLandscape: false)
Bogdan Timofte authored 2 months ago
485
        case .chart:
Bogdan Timofte authored 2 months ago
486
            MeterChartTabView(size: size, isLandscape: false)
Bogdan Timofte authored a month ago
487
        case .chargeRecord:
Bogdan Timofte authored a month ago
488
            MeterChargeRecordTabView().equatable()
Bogdan Timofte authored a month ago
489
        case .dataGroups:
490
            MeterDataGroupsTabView()
Bogdan Timofte authored 2 months ago
491
        case .settings:
Bogdan Timofte authored 2 months ago
492
            MeterSettingsTabView(isMacIPadApp: Self.isMacIPadApp) {
493
                withAnimation(.easeInOut(duration: 0.22)) {
Bogdan Timofte authored 2 months ago
494
                    selectedMeterTab = .home
Bogdan Timofte authored 2 months ago
495
                }
496
            }
Bogdan Timofte authored 2 months ago
497
        }
498
    }
499

            
500
    private var availableMeterTabs: [MeterTab] {
Bogdan Timofte authored a month ago
501
        var tabs: [MeterTab] = [.home, .live, .chart]
502
        if meter.supportsRecordingView {
503
            tabs.append(.chargeRecord)
504
        }
505
        tabs.append(.dataGroups)
506
        tabs.append(.settings)
507
        return tabs
Bogdan Timofte authored 2 months ago
508
    }
509

            
Bogdan Timofte authored 2 months ago
510
    private var displayedMeterTab: MeterTab {
511
        if availableMeterTabs.contains(selectedMeterTab) {
512
            return selectedMeterTab
513
        }
514
        return .home
515
    }
516

            
517
    private func restoreSelectedTab() {
518
        guard let restoredTab = MeterTab(rawValue: meter.preferredTabIdentifier) else {
519
            meter.preferredTabIdentifier = MeterTab.home.rawValue
520
            selectedMeterTab = .home
Bogdan Timofte authored 2 months ago
521
            return
522
        }
Bogdan Timofte authored 2 months ago
523

            
Bogdan Timofte authored a month ago
524
        selectedMeterTab = availableMeterTabs.contains(restoredTab) ? restoredTab : .home
Bogdan Timofte authored 2 months ago
525
    }
526

            
Bogdan Timofte authored 2 months ago
527
    private var meterBackground: some View {
528
        LinearGradient(
529
            colors: [
530
                meter.color.opacity(0.22),
531
                Color.secondary.opacity(0.08),
532
                Color.clear
533
            ],
534
            startPoint: .topLeading,
535
            endPoint: .bottomTrailing
536
        )
537
        .ignoresSafeArea()
538
    }
539

            
540
    private func isLandscape(size: CGSize) -> Bool {
541
        size.width > size.height
542
    }
543

            
Bogdan Timofte authored 2 months ago
544
    private func tabBarStyle(for landscape: Bool, usesOverlayTabBar: Bool, size: CGSize) -> TabBarStyle {
Bogdan Timofte authored 2 months ago
545
        if usesOverlayTabBar {
546
            return .landscapeFloating
547
        }
548

            
549
        if landscape {
550
            return .landscapeInline
551
        }
552

            
Bogdan Timofte authored 2 months ago
553
        if Self.isPhone && size.width < 390 {
554
            return .portraitCompact
555
        }
556

            
Bogdan Timofte authored 2 months ago
557
        return .portrait
558
    }
559

            
560
    private func landscapeContentTopPadding(for style: TabBarStyle) -> CGFloat {
561
        if style.floatingInset > 0 {
562
            return max(landscapeTabBarHeight * 0.44, 26)
563
        }
564

            
565
        return max(landscapeTabBarHeight - 6, 0)
566
    }
567

            
Bogdan Timofte authored a month ago
568
    private func title(for tab: MeterTab) -> String {
569
        switch tab {
570
        case .home:
571
            return "Home"
572
        case .live:
573
            return "Live"
574
        case .chart:
575
            return "Chart"
576
        case .chargeRecord:
577
            return "Charge Record"
578
        case .dataGroups:
579
            return meter.dataGroupsTitle
580
        case .settings:
581
            return "Settings"
582
        }
583
    }
584

            
Bogdan Timofte authored 2 months ago
585
}
586

            
587
private struct MeterTabBarHeightPreferenceKey: PreferenceKey {
588
    static var defaultValue: CGFloat = 0
589

            
590
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
591
        value = max(value, nextValue())
592
    }
Bogdan Timofte authored 2 months ago
593
}
594

            
Bogdan Timofte authored 2 months ago
595
// MARK: - Conditional navigation bar modifier (skipped on Designed-for-iPad / Mac)
596

            
597
private struct IOSOnlyNavBar: ViewModifier {
598
    let apply: Bool
599
    let title: String
600
    let showRSSI: Bool
601
    let rssi: Int
602
    let meter: Meter
603

            
604
    @ViewBuilder
605
    func body(content: Content) -> some View {
606
        if apply {
607
            content
608
                .navigationBarTitle(title)
609
                .toolbar {
610
                    ToolbarItemGroup(placement: .navigationBarTrailing) {
Bogdan Timofte authored 2 months ago
611
                        MeterConnectionToolbarButton(
612
                            operationalState: meter.operationalState,
613
                            showsTitle: false,
614
                            connectAction: { meter.connect() },
615
                            disconnectAction: { meter.disconnect() }
616
                        )
617
                        .font(.body.weight(.semibold))
Bogdan Timofte authored 2 months ago
618
                        if showRSSI {
619
                            RSSIView(RSSI: rssi)
620
                                .frame(width: 18, height: 18)
621
                        }
622
                    }
623
                }
624
        } else {
625
            content
626
        }
627
    }
628
}