Newer Older
686 lines | 23.307kb
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 horizontalPadding: CGFloat
16
        let topPadding: CGFloat
17
        let bottomPadding: CGFloat
18
        let chipHorizontalPadding: CGFloat
19
        let chipVerticalPadding: CGFloat
20
        let outerPadding: CGFloat
21
        let barBackgroundOpacity: CGFloat
22
        let materialOpacity: CGFloat
23
        let shadowOpacity: CGFloat
24
        let floatingInset: CGFloat
25

            
26
        static let portrait = TabBarStyle(
27
            horizontalPadding: 16,
28
            topPadding: 10,
29
            bottomPadding: 8,
30
            chipHorizontalPadding: 10,
31
            chipVerticalPadding: 7,
32
            outerPadding: 6,
33
            barBackgroundOpacity: 0.10,
34
            materialOpacity: 0.78,
35
            shadowOpacity: 0,
36
            floatingInset: 0
37
        )
38

            
Bogdan Timofte authored 2 months ago
39
        static let portraitCompact = TabBarStyle(
40
            horizontalPadding: 16,
41
            topPadding: 10,
42
            bottomPadding: 8,
43
            chipHorizontalPadding: 12,
44
            chipVerticalPadding: 10,
45
            outerPadding: 6,
46
            barBackgroundOpacity: 0.14,
47
            materialOpacity: 0.90,
48
            shadowOpacity: 0,
49
            floatingInset: 0
50
        )
51

            
Bogdan Timofte authored 2 months ago
52
        static let landscapeInline = TabBarStyle(
53
            horizontalPadding: 12,
54
            topPadding: 10,
55
            bottomPadding: 8,
56
            chipHorizontalPadding: 10,
57
            chipVerticalPadding: 7,
58
            outerPadding: 6,
59
            barBackgroundOpacity: 0.10,
60
            materialOpacity: 0.78,
61
            shadowOpacity: 0,
62
            floatingInset: 0
63
        )
64

            
65
        static let landscapeFloating = TabBarStyle(
66
            horizontalPadding: 16,
67
            topPadding: 10,
68
            bottomPadding: 0,
69
            chipHorizontalPadding: 11,
70
            chipVerticalPadding: 11,
71
            outerPadding: 7,
Bogdan Timofte authored 2 months ago
72
            barBackgroundOpacity: 0.16,
73
            materialOpacity: 0.88,
74
            shadowOpacity: 0.12,
Bogdan Timofte authored 2 months ago
75
            floatingInset: 12
76
        )
77
    }
78

            
Bogdan Timofte authored 2 months ago
79
    private enum MeterTab: String, Hashable {
Bogdan Timofte authored 2 months ago
80
        case home
Bogdan Timofte authored 2 months ago
81
        case live
82
        case chart
Bogdan Timofte authored a month ago
83
        case chargeRecord
84
        case dataGroups
Bogdan Timofte authored 2 months ago
85
        case settings
Bogdan Timofte authored 2 months ago
86

            
87
        var systemImage: String {
88
            switch self {
Bogdan Timofte authored 2 months ago
89
            case .home: return "house.fill"
Bogdan Timofte authored 2 months ago
90
            case .live: return "waveform.path.ecg"
91
            case .chart: return "chart.xyaxis.line"
Bogdan Timofte authored a month ago
92
            case .chargeRecord: return "gauge.with.dots.needle.50percent"
93
            case .dataGroups: return "square.grid.2x2.fill"
Bogdan Timofte authored 2 months ago
94
            case .settings: return "gearshape.fill"
Bogdan Timofte authored 2 months ago
95
            }
96
        }
97
    }
Bogdan Timofte authored 2 months ago
98

            
99
    @EnvironmentObject private var meter: Meter
Bogdan Timofte authored 2 months ago
100
    @Environment(\.dismiss) private var dismiss
101

            
102
    private static let isMacIPadApp: Bool = ProcessInfo.processInfo.isiOSAppOnMac
Bogdan Timofte authored 2 months ago
103
    #if os(iOS)
104
    private static let isPhone: Bool = UIDevice.current.userInterfaceIdiom == .phone
105
    #else
106
    private static let isPhone: Bool = false
107
    #endif
Bogdan Timofte authored 2 months ago
108

            
Bogdan Timofte authored 2 months ago
109
    @State private var selectedMeterTab: MeterTab = .home
Bogdan Timofte authored 2 months ago
110
    @State private var navBarTitle: String = "Meter"
111
    @State private var navBarShowRSSI: Bool = false
112
    @State private var navBarRSSI: Int = 0
Bogdan Timofte authored 2 months ago
113
    @State private var landscapeTabBarHeight: CGFloat = 0
Bogdan Timofte authored 2 months ago
114

            
Bogdan Timofte authored 2 months ago
115
    var body: some View {
Bogdan Timofte authored 2 months ago
116
        GeometryReader { proxy in
117
            let landscape = isLandscape(size: proxy.size)
Bogdan Timofte authored 2 months ago
118
            let usesOverlayTabBar = landscape && Self.isPhone
Bogdan Timofte authored 2 months ago
119
            let tabBarStyle = tabBarStyle(
120
                for: landscape,
121
                usesOverlayTabBar: usesOverlayTabBar,
122
                size: proxy.size
123
            )
Bogdan Timofte authored a month ago
124
            let tabBarPresentation = tabBarPresentation(
125
                for: proxy.size,
126
                usesOverlayTabBar: usesOverlayTabBar
127
            )
Bogdan Timofte authored 2 months ago
128

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

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

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

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

            
204
            Spacer()
205

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

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

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

            
Bogdan Timofte authored a month ago
233
    private func portraitContent(
234
        size: CGSize,
235
        tabBarStyle: TabBarStyle,
236
        tabBarPresentation: AdaptiveTabBarPresentation
237
    ) -> some View {
238
        portraitSegmentedDeck(
239
            size: size,
240
            tabBarStyle: tabBarStyle,
241
            tabBarPresentation: tabBarPresentation
242
        )
Bogdan Timofte authored 2 months ago
243
    }
244

            
245
    @ViewBuilder
Bogdan Timofte authored a month ago
246
    private func landscapeDeck(
247
        size: CGSize,
248
        usesOverlayTabBar: Bool,
249
        tabBarStyle: TabBarStyle,
250
        tabBarPresentation: AdaptiveTabBarPresentation
251
    ) -> some View {
Bogdan Timofte authored 2 months ago
252
        if usesOverlayTabBar {
Bogdan Timofte authored a month ago
253
            landscapeOverlaySegmentedDeck(
254
                size: size,
255
                tabBarStyle: tabBarStyle,
256
                tabBarPresentation: tabBarPresentation
257
            )
Bogdan Timofte authored 2 months ago
258
        } else {
Bogdan Timofte authored a month ago
259
            landscapeSegmentedDeck(
260
                size: size,
261
                tabBarStyle: tabBarStyle,
262
                tabBarPresentation: tabBarPresentation
263
            )
Bogdan Timofte authored 2 months ago
264
        }
Bogdan Timofte authored 2 months ago
265
    }
266

            
Bogdan Timofte authored a month ago
267
    private func landscapeOverlaySegmentedDeck(
268
        size: CGSize,
269
        tabBarStyle: TabBarStyle,
270
        tabBarPresentation: AdaptiveTabBarPresentation
271
    ) -> some View {
Bogdan Timofte authored 2 months ago
272
        ZStack(alignment: .top) {
273
            landscapeSegmentedContent(size: size)
274
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
275
                .padding(.top, landscapeContentTopPadding(for: tabBarStyle))
Bogdan Timofte authored 2 months ago
276
                .id(displayedMeterTab)
Bogdan Timofte authored 2 months ago
277
                .transition(.opacity.combined(with: .move(edge: .trailing)))
278

            
Bogdan Timofte authored a month ago
279
            segmentedTabBar(
280
                style: tabBarStyle,
281
                presentation: tabBarPresentation,
282
                showsConnectionAction: !Self.isMacIPadApp
283
            )
Bogdan Timofte authored 2 months ago
284
        }
Bogdan Timofte authored 2 months ago
285
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored 2 months ago
286
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
287
        .onAppear {
Bogdan Timofte authored 2 months ago
288
            restoreSelectedTab()
Bogdan Timofte authored 2 months ago
289
        }
290
        .onPreferenceChange(MeterTabBarHeightPreferenceKey.self) { height in
291
            if height > 0 {
292
                landscapeTabBarHeight = height
293
            }
294
        }
Bogdan Timofte authored 2 months ago
295
    }
296

            
Bogdan Timofte authored a month ago
297
    private func landscapeSegmentedDeck(
298
        size: CGSize,
299
        tabBarStyle: TabBarStyle,
300
        tabBarPresentation: AdaptiveTabBarPresentation
301
    ) -> some View {
Bogdan Timofte authored 2 months ago
302
        VStack(spacing: 0) {
Bogdan Timofte authored a month ago
303
            segmentedTabBar(
304
                style: tabBarStyle,
305
                presentation: tabBarPresentation,
306
                showsConnectionAction: !Self.isMacIPadApp
307
            )
Bogdan Timofte authored 2 months ago
308

            
309
            landscapeSegmentedContent(size: size)
310
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Bogdan Timofte authored 2 months ago
311
                .id(displayedMeterTab)
Bogdan Timofte authored 2 months ago
312
                .transition(.opacity.combined(with: .move(edge: .trailing)))
313
        }
Bogdan Timofte authored 2 months ago
314
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored 2 months ago
315
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
316
        .onAppear {
Bogdan Timofte authored 2 months ago
317
            restoreSelectedTab()
Bogdan Timofte authored 2 months ago
318
        }
319
    }
320

            
Bogdan Timofte authored a month ago
321
    private func portraitSegmentedDeck(
322
        size: CGSize,
323
        tabBarStyle: TabBarStyle,
324
        tabBarPresentation: AdaptiveTabBarPresentation
325
    ) -> some View {
Bogdan Timofte authored 2 months ago
326
        VStack(spacing: 0) {
Bogdan Timofte authored a month ago
327
            segmentedTabBar(
328
                style: tabBarStyle,
329
                presentation: tabBarPresentation
330
            )
Bogdan Timofte authored 2 months ago
331

            
332
            portraitSegmentedContent(size: size)
333
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Bogdan Timofte authored 2 months ago
334
                .id(displayedMeterTab)
Bogdan Timofte authored 2 months ago
335
                .transition(.opacity.combined(with: .move(edge: .trailing)))
336
        }
Bogdan Timofte authored 2 months ago
337
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored 2 months ago
338
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
339
        .onAppear {
Bogdan Timofte authored 2 months ago
340
            restoreSelectedTab()
Bogdan Timofte authored 2 months ago
341
        }
342
    }
343

            
Bogdan Timofte authored a month ago
344
    private func segmentedTabBar(
345
        style: TabBarStyle,
346
        presentation: AdaptiveTabBarPresentation,
347
        showsConnectionAction: Bool = false
348
    ) -> some View {
Bogdan Timofte authored 2 months ago
349
        let isFloating = style.floatingInset > 0
Bogdan Timofte authored a month ago
350
        let cornerRadius = presentation.showsTitles ? 14.0 : 22.0
Bogdan Timofte authored 2 months ago
351
        let unselectedForegroundColor = isFloating ? Color.primary.opacity(0.86) : Color.primary
352
        let unselectedChipFill = isFloating ? Color.black.opacity(0.08) : Color.secondary.opacity(0.12)
Bogdan Timofte authored 2 months ago
353

            
354
        return HStack {
Bogdan Timofte authored 2 months ago
355
            Spacer(minLength: 0)
356

            
357
            HStack(spacing: 8) {
358
                ForEach(availableMeterTabs, id: \.self) { tab in
Bogdan Timofte authored 2 months ago
359
                    let isSelected = displayedMeterTab == tab
Bogdan Timofte authored 2 months ago
360

            
361
                    Button {
362
                        withAnimation(.easeInOut(duration: 0.2)) {
363
                            selectedMeterTab = tab
364
                        }
365
                    } label: {
366
                        HStack(spacing: 6) {
367
                            Image(systemName: tab.systemImage)
368
                                .font(.subheadline.weight(.semibold))
Bogdan Timofte authored a month ago
369
                            if presentation.showsTitles {
Bogdan Timofte authored a month ago
370
                                Text(title(for: tab))
Bogdan Timofte authored 2 months ago
371
                                    .font(.subheadline.weight(.semibold))
372
                                    .lineLimit(1)
373
                            }
Bogdan Timofte authored 2 months ago
374
                        }
Bogdan Timofte authored 2 months ago
375
                        .foregroundColor(
376
                            isSelected
377
                            ? .white
Bogdan Timofte authored 2 months ago
378
                            : unselectedForegroundColor
Bogdan Timofte authored 2 months ago
379
                        )
380
                        .padding(.horizontal, style.chipHorizontalPadding)
381
                        .padding(.vertical, style.chipVerticalPadding)
Bogdan Timofte authored 2 months ago
382
                        .frame(maxWidth: .infinity)
383
                        .background(
384
                            Capsule()
Bogdan Timofte authored 2 months ago
385
                                .fill(
386
                                    isSelected
387
                                    ? meter.color.opacity(isFloating ? 0.94 : 1)
Bogdan Timofte authored 2 months ago
388
                                    : unselectedChipFill
Bogdan Timofte authored 2 months ago
389
                                )
Bogdan Timofte authored 2 months ago
390
                        )
Bogdan Timofte authored 2 months ago
391
                    }
Bogdan Timofte authored 2 months ago
392
                    .buttonStyle(.plain)
Bogdan Timofte authored a month ago
393
                    .accessibilityLabel(title(for: tab))
Bogdan Timofte authored 2 months ago
394
                }
395
            }
Bogdan Timofte authored a month ago
396
            .frame(maxWidth: presentation.maxWidth)
Bogdan Timofte authored 2 months ago
397
            .padding(style.outerPadding)
Bogdan Timofte authored 2 months ago
398
            .background(
Bogdan Timofte authored 2 months ago
399
                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
400
                    .fill(
401
                        isFloating
402
                        ? LinearGradient(
403
                            colors: [
Bogdan Timofte authored 2 months ago
404
                                Color.white.opacity(0.76),
405
                                Color.white.opacity(0.52)
Bogdan Timofte authored 2 months ago
406
                            ],
407
                            startPoint: .topLeading,
408
                            endPoint: .bottomTrailing
409
                        )
410
                        : LinearGradient(
411
                            colors: [
412
                                Color.secondary.opacity(style.barBackgroundOpacity),
413
                                Color.secondary.opacity(style.barBackgroundOpacity)
414
                            ],
415
                            startPoint: .topLeading,
416
                            endPoint: .bottomTrailing
417
                        )
418
                    )
Bogdan Timofte authored 2 months ago
419
            )
Bogdan Timofte authored 2 months ago
420
            .overlay {
421
                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
422
                    .stroke(
Bogdan Timofte authored 2 months ago
423
                        isFloating ? Color.black.opacity(0.08) : Color.clear,
Bogdan Timofte authored 2 months ago
424
                        lineWidth: 1
425
                    )
426
            }
427
            .background {
Bogdan Timofte authored 2 months ago
428
                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
429
                    .fill(.ultraThinMaterial)
430
                    .opacity(style.materialOpacity)
Bogdan Timofte authored 2 months ago
431
            }
432
            .shadow(color: Color.black.opacity(style.shadowOpacity), radius: isFloating ? 28 : 24, x: 0, y: isFloating ? 16 : 12)
Bogdan Timofte authored 2 months ago
433

            
434
            Spacer(minLength: 0)
435
        }
Bogdan Timofte authored 2 months ago
436
        .padding(.horizontal, style.horizontalPadding)
437
        .padding(.top, style.topPadding)
438
        .padding(.bottom, style.bottomPadding)
Bogdan Timofte authored 2 months ago
439
        .background(
Bogdan Timofte authored 2 months ago
440
            GeometryReader { geometry in
441
                Color.clear
442
                    .preference(key: MeterTabBarHeightPreferenceKey.self, value: geometry.size.height)
443
            }
Bogdan Timofte authored 2 months ago
444
        )
Bogdan Timofte authored 2 months ago
445
        .padding(.horizontal, style.floatingInset)
446
        .background {
447
            if style.floatingInset == 0 {
448
                Rectangle()
449
                    .fill(.ultraThinMaterial)
450
                    .opacity(style.materialOpacity)
451
                    .ignoresSafeArea(edges: .top)
452
            }
453
        }
Bogdan Timofte authored 2 months ago
454
        .overlay(alignment: .bottom) {
Bogdan Timofte authored 2 months ago
455
            if style.floatingInset == 0 {
456
                Rectangle()
457
                    .fill(Color.secondary.opacity(0.12))
458
                    .frame(height: 1)
459
            }
460
        }
461
        .overlay(alignment: .trailing) {
462
            if showsConnectionAction {
463
                MeterConnectionToolbarButton(
464
                    operationalState: meter.operationalState,
465
                    showsTitle: false,
466
                    connectAction: { meter.connect() },
467
                    disconnectAction: { meter.disconnect() }
468
                )
469
                .font(.title3.weight(.semibold))
470
                .padding(.trailing, style.horizontalPadding + style.floatingInset + 4)
471
                .padding(.top, style.topPadding)
472
                .padding(.bottom, style.bottomPadding)
473
            }
Bogdan Timofte authored 2 months ago
474
        }
Bogdan Timofte authored 2 months ago
475
    }
476

            
Bogdan Timofte authored 2 months ago
477
    @ViewBuilder
478
    private func landscapeSegmentedContent(size: CGSize) -> some View {
Bogdan Timofte authored 2 months ago
479
        switch displayedMeterTab {
Bogdan Timofte authored 2 months ago
480
        case .home:
Bogdan Timofte authored a month ago
481
            MeterHomeTabView(
482
                size: size,
483
                isLandscape: true,
484
                showChargeRecordTab: {
485
                    withAnimation(.easeInOut(duration: 0.22)) {
486
                        selectedMeterTab = .chargeRecord
487
                    }
488
                },
489
                showDataGroupsTab: {
490
                    withAnimation(.easeInOut(duration: 0.22)) {
491
                        selectedMeterTab = .dataGroups
492
                    }
493
                }
494
            )
Bogdan Timofte authored 2 months ago
495
        case .live:
Bogdan Timofte authored 2 months ago
496
            MeterLiveTabView(size: size, isLandscape: true)
Bogdan Timofte authored 2 months ago
497
        case .chart:
Bogdan Timofte authored 2 months ago
498
            MeterChartTabView(size: size, isLandscape: true)
Bogdan Timofte authored a month ago
499
        case .chargeRecord:
Bogdan Timofte authored a month ago
500
            MeterChargeRecordTabView().equatable()
Bogdan Timofte authored a month ago
501
        case .dataGroups:
502
            MeterDataGroupsTabView()
Bogdan Timofte authored 2 months ago
503
        case .settings:
Bogdan Timofte authored 2 months ago
504
            MeterSettingsTabView(isMacIPadApp: Self.isMacIPadApp) {
505
                withAnimation(.easeInOut(duration: 0.22)) {
Bogdan Timofte authored 2 months ago
506
                    selectedMeterTab = .home
Bogdan Timofte authored 2 months ago
507
                }
508
            }
Bogdan Timofte authored 2 months ago
509
        }
510
    }
Bogdan Timofte authored 2 months ago
511

            
Bogdan Timofte authored 2 months ago
512
    @ViewBuilder
513
    private func portraitSegmentedContent(size: CGSize) -> some View {
Bogdan Timofte authored 2 months ago
514
        switch displayedMeterTab {
Bogdan Timofte authored 2 months ago
515
        case .home:
Bogdan Timofte authored a month ago
516
            MeterHomeTabView(
517
                size: size,
518
                isLandscape: false,
519
                showChargeRecordTab: {
520
                    withAnimation(.easeInOut(duration: 0.22)) {
521
                        selectedMeterTab = .chargeRecord
522
                    }
523
                },
524
                showDataGroupsTab: {
525
                    withAnimation(.easeInOut(duration: 0.22)) {
526
                        selectedMeterTab = .dataGroups
527
                    }
528
                }
529
            )
Bogdan Timofte authored 2 months ago
530
        case .live:
Bogdan Timofte authored 2 months ago
531
            MeterLiveTabView(size: size, isLandscape: false)
Bogdan Timofte authored 2 months ago
532
        case .chart:
Bogdan Timofte authored 2 months ago
533
            MeterChartTabView(size: size, isLandscape: false)
Bogdan Timofte authored a month ago
534
        case .chargeRecord:
Bogdan Timofte authored a month ago
535
            MeterChargeRecordTabView().equatable()
Bogdan Timofte authored a month ago
536
        case .dataGroups:
537
            MeterDataGroupsTabView()
Bogdan Timofte authored 2 months ago
538
        case .settings:
Bogdan Timofte authored 2 months ago
539
            MeterSettingsTabView(isMacIPadApp: Self.isMacIPadApp) {
540
                withAnimation(.easeInOut(duration: 0.22)) {
Bogdan Timofte authored 2 months ago
541
                    selectedMeterTab = .home
Bogdan Timofte authored 2 months ago
542
                }
543
            }
Bogdan Timofte authored 2 months ago
544
        }
545
    }
546

            
547
    private var availableMeterTabs: [MeterTab] {
Bogdan Timofte authored a month ago
548
        var tabs: [MeterTab] = [.home, .live, .chart]
549
        if meter.supportsRecordingView {
550
            tabs.append(.chargeRecord)
551
        }
552
        tabs.append(.dataGroups)
553
        tabs.append(.settings)
554
        return tabs
Bogdan Timofte authored 2 months ago
555
    }
556

            
Bogdan Timofte authored 2 months ago
557
    private var displayedMeterTab: MeterTab {
558
        if availableMeterTabs.contains(selectedMeterTab) {
559
            return selectedMeterTab
560
        }
561
        return .home
562
    }
563

            
564
    private func restoreSelectedTab() {
565
        guard let restoredTab = MeterTab(rawValue: meter.preferredTabIdentifier) else {
566
            meter.preferredTabIdentifier = MeterTab.home.rawValue
567
            selectedMeterTab = .home
Bogdan Timofte authored 2 months ago
568
            return
569
        }
Bogdan Timofte authored 2 months ago
570

            
Bogdan Timofte authored a month ago
571
        selectedMeterTab = availableMeterTabs.contains(restoredTab) ? restoredTab : .home
Bogdan Timofte authored 2 months ago
572
    }
573

            
Bogdan Timofte authored 2 months ago
574
    private var meterBackground: some View {
575
        LinearGradient(
576
            colors: [
577
                meter.color.opacity(0.22),
578
                Color.secondary.opacity(0.08),
579
                Color.clear
580
            ],
581
            startPoint: .topLeading,
582
            endPoint: .bottomTrailing
583
        )
584
        .ignoresSafeArea()
585
    }
586

            
587
    private func isLandscape(size: CGSize) -> Bool {
588
        size.width > size.height
589
    }
590

            
Bogdan Timofte authored 2 months ago
591
    private func tabBarStyle(for landscape: Bool, usesOverlayTabBar: Bool, size: CGSize) -> TabBarStyle {
Bogdan Timofte authored 2 months ago
592
        if usesOverlayTabBar {
593
            return .landscapeFloating
594
        }
595

            
596
        if landscape {
597
            return .landscapeInline
598
        }
599

            
Bogdan Timofte authored 2 months ago
600
        if Self.isPhone && size.width < 390 {
601
            return .portraitCompact
602
        }
603

            
Bogdan Timofte authored 2 months ago
604
        return .portrait
605
    }
606

            
Bogdan Timofte authored a month ago
607
    private func tabBarPresentation(for size: CGSize, usesOverlayTabBar: Bool) -> AdaptiveTabBarPresentation {
608
        if usesOverlayTabBar {
609
            return AdaptiveTabBarPresentation(
610
                showsTitles: false,
611
                maxWidth: 260
612
            )
613
        }
614

            
615
        return AdaptiveTabBarPresentation.standard(for: size)
616
    }
617

            
Bogdan Timofte authored 2 months ago
618
    private func landscapeContentTopPadding(for style: TabBarStyle) -> CGFloat {
619
        if style.floatingInset > 0 {
620
            return max(landscapeTabBarHeight * 0.44, 26)
621
        }
622

            
623
        return max(landscapeTabBarHeight - 6, 0)
624
    }
625

            
Bogdan Timofte authored a month ago
626
    private func title(for tab: MeterTab) -> String {
627
        switch tab {
628
        case .home:
629
            return "Home"
630
        case .live:
631
            return "Live"
632
        case .chart:
633
            return "Chart"
634
        case .chargeRecord:
635
            return "Charge Record"
636
        case .dataGroups:
637
            return meter.dataGroupsTitle
638
        case .settings:
639
            return "Settings"
640
        }
641
    }
642

            
Bogdan Timofte authored 2 months ago
643
}
644

            
645
private struct MeterTabBarHeightPreferenceKey: PreferenceKey {
646
    static var defaultValue: CGFloat = 0
647

            
648
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
649
        value = max(value, nextValue())
650
    }
Bogdan Timofte authored 2 months ago
651
}
652

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

            
655
private struct IOSOnlyNavBar: ViewModifier {
656
    let apply: Bool
657
    let title: String
658
    let showRSSI: Bool
659
    let rssi: Int
660
    let meter: Meter
661

            
662
    @ViewBuilder
663
    func body(content: Content) -> some View {
664
        if apply {
665
            content
666
                .navigationBarTitle(title)
667
                .toolbar {
668
                    ToolbarItemGroup(placement: .navigationBarTrailing) {
Bogdan Timofte authored 2 months ago
669
                        MeterConnectionToolbarButton(
670
                            operationalState: meter.operationalState,
671
                            showsTitle: false,
672
                            connectAction: { meter.connect() },
673
                            disconnectAction: { meter.disconnect() }
674
                        )
675
                        .font(.body.weight(.semibold))
Bogdan Timofte authored 2 months ago
676
                        if showRSSI {
677
                            RSSIView(RSSI: rssi)
678
                                .frame(width: 18, height: 18)
679
                        }
680
                    }
681
                }
682
        } else {
683
            content
684
        }
685
    }
686
}