Newer Older
697 lines | 23.867kb
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 a month ago
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
Bogdan Timofte authored 2 months ago
111

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

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

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

            
190
    // MARK: - Custom navigation header for Designed-for-iPad on Mac
191

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

            
206
            Text(meter.name.isEmpty ? "Meter" : meter.name)
207
                .font(.headline)
208
                .lineLimit(1)
209

            
210
            Spacer()
211

            
Bogdan Timofte authored 2 months ago
212
            MeterConnectionToolbarButton(
213
                operationalState: meter.operationalState,
214
                showsTitle: true,
215
                connectAction: { meter.connect() },
216
                disconnectAction: { meter.disconnect() }
217
            )
218

            
Bogdan Timofte authored 2 months ago
219
            if meter.operationalState > .notPresent {
Bogdan Timofte authored 2 months ago
220
                RSSIView(RSSI: meter.btSerial.averageRSSI)
Bogdan Timofte authored 2 months ago
221
                    .frame(width: 18, height: 18)
222
            }
Bogdan Timofte authored 2 months ago
223

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

            
Bogdan Timofte authored a month ago
239
    private func portraitContent(
240
        size: CGSize,
241
        tabBarStyle: TabBarStyle,
242
        tabBarPresentation: AdaptiveTabBarPresentation
243
    ) -> some View {
244
        portraitSegmentedDeck(
245
            size: size,
246
            tabBarStyle: tabBarStyle,
247
            tabBarPresentation: tabBarPresentation
248
        )
Bogdan Timofte authored 2 months ago
249
    }
250

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

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

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

            
Bogdan Timofte authored a month ago
303
    private func landscapeSegmentedDeck(
304
        size: CGSize,
305
        tabBarStyle: TabBarStyle,
306
        tabBarPresentation: AdaptiveTabBarPresentation
307
    ) -> some View {
Bogdan Timofte authored 2 months ago
308
        VStack(spacing: 0) {
Bogdan Timofte authored a month ago
309
            segmentedTabBar(
310
                style: tabBarStyle,
311
                presentation: tabBarPresentation,
312
                showsConnectionAction: !Self.isMacIPadApp
313
            )
Bogdan Timofte authored 2 months ago
314

            
315
            landscapeSegmentedContent(size: size)
316
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Bogdan Timofte authored 2 months ago
317
                .id(displayedMeterTab)
Bogdan Timofte authored 2 months ago
318
                .transition(.opacity.combined(with: .move(edge: .trailing)))
319
        }
Bogdan Timofte authored 2 months ago
320
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored 2 months ago
321
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
322
        .onAppear {
Bogdan Timofte authored 2 months ago
323
            restoreSelectedTab()
Bogdan Timofte authored 2 months ago
324
        }
325
    }
326

            
Bogdan Timofte authored a month ago
327
    private func portraitSegmentedDeck(
328
        size: CGSize,
329
        tabBarStyle: TabBarStyle,
330
        tabBarPresentation: AdaptiveTabBarPresentation
331
    ) -> some View {
Bogdan Timofte authored 2 months ago
332
        VStack(spacing: 0) {
Bogdan Timofte authored a month ago
333
            segmentedTabBar(
334
                style: tabBarStyle,
335
                presentation: tabBarPresentation
336
            )
Bogdan Timofte authored 2 months ago
337

            
338
            portraitSegmentedContent(size: size)
339
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Bogdan Timofte authored 2 months ago
340
                .id(displayedMeterTab)
Bogdan Timofte authored 2 months ago
341
                .transition(.opacity.combined(with: .move(edge: .trailing)))
342
        }
Bogdan Timofte authored 2 months ago
343
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored 2 months ago
344
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
345
        .onAppear {
Bogdan Timofte authored 2 months ago
346
            restoreSelectedTab()
Bogdan Timofte authored 2 months ago
347
        }
348
    }
349

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

            
360
        return HStack {
Bogdan Timofte authored 2 months ago
361
            Spacer(minLength: 0)
362

            
363
            HStack(spacing: 8) {
364
                ForEach(availableMeterTabs, id: \.self) { tab in
Bogdan Timofte authored 2 months ago
365
                    let isSelected = displayedMeterTab == tab
Bogdan Timofte authored 2 months ago
366

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

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

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

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

            
553
    private var availableMeterTabs: [MeterTab] {
Bogdan Timofte authored a month ago
554
        var tabs: [MeterTab] = [.home, .live, .chart]
555
        if meter.supportsRecordingView {
556
            tabs.append(.chargeRecord)
557
        }
558
        tabs.append(.dataGroups)
559
        tabs.append(.settings)
560
        return tabs
Bogdan Timofte authored 2 months ago
561
    }
562

            
Bogdan Timofte authored 2 months ago
563
    private var displayedMeterTab: MeterTab {
564
        if availableMeterTabs.contains(selectedMeterTab) {
565
            return selectedMeterTab
566
        }
567
        return .home
568
    }
569

            
570
    private func restoreSelectedTab() {
571
        guard let restoredTab = MeterTab(rawValue: meter.preferredTabIdentifier) else {
572
            meter.preferredTabIdentifier = MeterTab.home.rawValue
573
            selectedMeterTab = .home
Bogdan Timofte authored 2 months ago
574
            return
575
        }
Bogdan Timofte authored 2 months ago
576

            
Bogdan Timofte authored a month ago
577
        selectedMeterTab = availableMeterTabs.contains(restoredTab) ? restoredTab : .home
Bogdan Timofte authored 2 months ago
578
    }
579

            
Bogdan Timofte authored 2 months ago
580
    private var meterBackground: some View {
581
        LinearGradient(
582
            colors: [
583
                meter.color.opacity(0.22),
584
                Color.secondary.opacity(0.08),
585
                Color.clear
586
            ],
587
            startPoint: .topLeading,
588
            endPoint: .bottomTrailing
589
        )
590
        .ignoresSafeArea()
591
    }
592

            
593
    private func isLandscape(size: CGSize) -> Bool {
594
        size.width > size.height
595
    }
596

            
Bogdan Timofte authored 2 months ago
597
    private func tabBarStyle(for landscape: Bool, usesOverlayTabBar: Bool, size: CGSize) -> TabBarStyle {
Bogdan Timofte authored 2 months ago
598
        if usesOverlayTabBar {
599
            return .landscapeFloating
600
        }
601

            
602
        if landscape {
603
            return .landscapeInline
604
        }
605

            
Bogdan Timofte authored 2 months ago
606
        if Self.isPhone && size.width < 390 {
607
            return .portraitCompact
608
        }
609

            
Bogdan Timofte authored 2 months ago
610
        return .portrait
611
    }
612

            
Bogdan Timofte authored a month ago
613
    private func tabBarPresentation(for size: CGSize, usesOverlayTabBar: Bool) -> AdaptiveTabBarPresentation {
614
        if usesOverlayTabBar {
615
            return AdaptiveTabBarPresentation(
616
                showsTitles: false,
617
                maxWidth: 260
618
            )
619
        }
620

            
621
        return AdaptiveTabBarPresentation.standard(for: size)
622
    }
623

            
Bogdan Timofte authored 2 months ago
624
    private func landscapeContentTopPadding(for style: TabBarStyle) -> CGFloat {
625
        if style.floatingInset > 0 {
626
            return max(landscapeTabBarHeight * 0.44, 26)
627
        }
628

            
629
        return max(landscapeTabBarHeight - 6, 0)
630
    }
631

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

            
Bogdan Timofte authored 2 months ago
649
}
650

            
651
private struct MeterTabBarHeightPreferenceKey: PreferenceKey {
652
    static var defaultValue: CGFloat = 0
653

            
654
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
655
        value = max(value, nextValue())
656
    }
Bogdan Timofte authored 2 months ago
657
}
658

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

            
661
private struct IOSOnlyNavBar: ViewModifier {
662
    let apply: Bool
663
    let title: String
664
    let showRSSI: Bool
665
    let rssi: Int
666
    let meter: Meter
667

            
668
    @ViewBuilder
669
    func body(content: Content) -> some View {
670
        if apply {
671
            content
Bogdan Timofte authored a month ago
672
                .navigationBarTitle(title, displayMode: .inline)
Bogdan Timofte authored 2 months ago
673
                .toolbar {
674
                    ToolbarItemGroup(placement: .navigationBarTrailing) {
Bogdan Timofte authored 2 months ago
675
                        MeterConnectionToolbarButton(
676
                            operationalState: meter.operationalState,
677
                            showsTitle: false,
678
                            connectAction: { meter.connect() },
679
                            disconnectAction: { meter.disconnect() }
680
                        )
681
                        .font(.body.weight(.semibold))
Bogdan Timofte authored 2 months ago
682
                        if showRSSI {
683
                            RSSIView(RSSI: rssi)
684
                                .frame(width: 18, height: 18)
685
                        }
686
                    }
687
                }
Bogdan Timofte authored a month ago
688
                #if targetEnvironment(macCatalyst)
689
                .toolbar {
690
                    ToolbarItemGroup(placement: .primaryAction) {}
691
                }
692
                #endif
Bogdan Timofte authored 2 months ago
693
        } else {
694
            content
695
        }
696
    }
697
}