Newer Older
564 lines | 19.541kb
Bogdan Timofte authored 2 weeks 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 a week 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

            
43
        static let landscapeInline = TabBarStyle(
44
            showsTitles: true,
45
            horizontalPadding: 12,
46
            topPadding: 10,
47
            bottomPadding: 8,
48
            chipHorizontalPadding: 10,
49
            chipVerticalPadding: 7,
50
            outerPadding: 6,
51
            maxWidth: 420,
52
            barBackgroundOpacity: 0.10,
53
            materialOpacity: 0.78,
54
            shadowOpacity: 0,
55
            floatingInset: 0
56
        )
57

            
58
        static let landscapeFloating = TabBarStyle(
59
            showsTitles: false,
60
            horizontalPadding: 16,
61
            topPadding: 10,
62
            bottomPadding: 0,
63
            chipHorizontalPadding: 11,
64
            chipVerticalPadding: 11,
65
            outerPadding: 7,
66
            maxWidth: 260,
67
            barBackgroundOpacity: 0.02,
68
            materialOpacity: 0,
69
            shadowOpacity: 0.18,
70
            floatingInset: 12
71
        )
72
    }
73

            
Bogdan Timofte authored a week ago
74
    private enum MeterTab: String, Hashable {
Bogdan Timofte authored a week ago
75
        case home
Bogdan Timofte authored 2 weeks ago
76
        case live
77
        case chart
Bogdan Timofte authored a week ago
78
        case settings
Bogdan Timofte authored 2 weeks ago
79

            
80
        var title: String {
81
            switch self {
Bogdan Timofte authored a week ago
82
            case .home: return "Home"
Bogdan Timofte authored 2 weeks ago
83
            case .live: return "Live"
84
            case .chart: return "Chart"
Bogdan Timofte authored a week ago
85
            case .settings: return "Settings"
Bogdan Timofte authored 2 weeks ago
86
            }
87
        }
88

            
89
        var systemImage: String {
90
            switch self {
Bogdan Timofte authored a week ago
91
            case .home: return "house.fill"
Bogdan Timofte authored 2 weeks ago
92
            case .live: return "waveform.path.ecg"
93
            case .chart: return "chart.xyaxis.line"
Bogdan Timofte authored a week ago
94
            case .settings: return "gearshape.fill"
Bogdan Timofte authored 2 weeks ago
95
            }
96
        }
97
    }
Bogdan Timofte authored 2 weeks ago
98

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

            
102
    private static let isMacIPadApp: Bool = ProcessInfo.processInfo.isiOSAppOnMac
Bogdan Timofte authored a week 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 weeks ago
108

            
Bogdan Timofte authored a week ago
109
    @State private var selectedMeterTab: MeterTab = .home
Bogdan Timofte authored 2 weeks 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 a week ago
113
    @State private var landscapeTabBarHeight: CGFloat = 0
Bogdan Timofte authored 2 weeks ago
114

            
Bogdan Timofte authored 2 weeks ago
115
    var body: some View {
Bogdan Timofte authored 2 weeks ago
116
        GeometryReader { proxy in
117
            let landscape = isLandscape(size: proxy.size)
Bogdan Timofte authored a week ago
118
            let usesOverlayTabBar = landscape && Self.isPhone
119
            let tabBarStyle = tabBarStyle(for: landscape, usesOverlayTabBar: usesOverlayTabBar)
Bogdan Timofte authored 2 weeks ago
120

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

            
171
    // MARK: - Custom navigation header for Designed-for-iPad on Mac
172

            
173
    private var macNavigationHeader: some View {
174
        HStack(spacing: 12) {
175
            Button {
176
                dismiss()
177
            } label: {
178
                HStack(spacing: 4) {
179
                    Image(systemName: "chevron.left")
180
                        .font(.body.weight(.semibold))
181
                    Text("USB Meters")
182
                }
183
                .foregroundColor(.accentColor)
184
            }
185
            .buttonStyle(.plain)
186

            
187
            Text(meter.name.isEmpty ? "Meter" : meter.name)
188
                .font(.headline)
189
                .lineLimit(1)
190

            
191
            Spacer()
192

            
Bogdan Timofte authored a week ago
193
            MeterConnectionToolbarButton(
194
                operationalState: meter.operationalState,
195
                showsTitle: true,
196
                connectAction: { meter.connect() },
197
                disconnectAction: { meter.disconnect() }
198
            )
199

            
Bogdan Timofte authored 2 weeks ago
200
            if meter.operationalState > .notPresent {
Bogdan Timofte authored 2 weeks ago
201
                RSSIView(RSSI: meter.btSerial.averageRSSI)
Bogdan Timofte authored 2 weeks ago
202
                    .frame(width: 18, height: 18)
203
            }
Bogdan Timofte authored 2 weeks ago
204

            
205
        }
206
        .padding(.horizontal, 16)
207
        .padding(.vertical, 10)
208
        .background(
209
            Rectangle()
210
                .fill(.ultraThinMaterial)
211
                .ignoresSafeArea(edges: .top)
212
        )
213
        .overlay(alignment: .bottom) {
214
            Rectangle()
215
                .fill(Color.secondary.opacity(0.12))
216
                .frame(height: 1)
217
        }
Bogdan Timofte authored 2 weeks ago
218
    }
219

            
Bogdan Timofte authored a week ago
220
    private func portraitContent(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
221
        portraitSegmentedDeck(size: size, tabBarStyle: tabBarStyle)
222
    }
223

            
224
    @ViewBuilder
225
    private func landscapeDeck(size: CGSize, usesOverlayTabBar: Bool, tabBarStyle: TabBarStyle) -> some View {
226
        if usesOverlayTabBar {
227
            landscapeOverlaySegmentedDeck(size: size, tabBarStyle: tabBarStyle)
228
        } else {
229
            landscapeSegmentedDeck(size: size, tabBarStyle: tabBarStyle)
230
        }
Bogdan Timofte authored 2 weeks ago
231
    }
232

            
Bogdan Timofte authored a week ago
233
    private func landscapeOverlaySegmentedDeck(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
234
        ZStack(alignment: .top) {
235
            landscapeSegmentedContent(size: size)
236
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
237
                .padding(.top, landscapeContentTopPadding(for: tabBarStyle))
Bogdan Timofte authored a week ago
238
                .id(displayedMeterTab)
Bogdan Timofte authored a week ago
239
                .transition(.opacity.combined(with: .move(edge: .trailing)))
240

            
241
            segmentedTabBar(style: tabBarStyle, showsConnectionAction: !Self.isMacIPadApp)
242
        }
Bogdan Timofte authored a week ago
243
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored a week ago
244
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
245
        .onAppear {
Bogdan Timofte authored a week ago
246
            restoreSelectedTab()
Bogdan Timofte authored a week ago
247
        }
248
        .onPreferenceChange(MeterTabBarHeightPreferenceKey.self) { height in
249
            if height > 0 {
250
                landscapeTabBarHeight = height
251
            }
252
        }
Bogdan Timofte authored 2 weeks ago
253
    }
254

            
Bogdan Timofte authored a week ago
255
    private func landscapeSegmentedDeck(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
Bogdan Timofte authored 2 weeks ago
256
        VStack(spacing: 0) {
Bogdan Timofte authored a week ago
257
            segmentedTabBar(style: tabBarStyle, showsConnectionAction: !Self.isMacIPadApp)
Bogdan Timofte authored 2 weeks ago
258

            
259
            landscapeSegmentedContent(size: size)
260
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Bogdan Timofte authored a week ago
261
                .id(displayedMeterTab)
Bogdan Timofte authored 2 weeks ago
262
                .transition(.opacity.combined(with: .move(edge: .trailing)))
263
        }
Bogdan Timofte authored a week ago
264
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored 2 weeks ago
265
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
266
        .onAppear {
Bogdan Timofte authored a week ago
267
            restoreSelectedTab()
Bogdan Timofte authored 2 weeks ago
268
        }
269
    }
270

            
Bogdan Timofte authored a week ago
271
    private func portraitSegmentedDeck(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
Bogdan Timofte authored 2 weeks ago
272
        VStack(spacing: 0) {
Bogdan Timofte authored a week ago
273
            segmentedTabBar(style: tabBarStyle)
Bogdan Timofte authored 2 weeks ago
274

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

            
Bogdan Timofte authored a week ago
287
    private func segmentedTabBar(style: TabBarStyle, showsConnectionAction: Bool = false) -> some View {
288
        let isFloating = style.floatingInset > 0
289
        let cornerRadius = style.showsTitles ? 14.0 : 22.0
290

            
291
        return HStack {
Bogdan Timofte authored 2 weeks ago
292
            Spacer(minLength: 0)
293

            
294
            HStack(spacing: 8) {
295
                ForEach(availableMeterTabs, id: \.self) { tab in
Bogdan Timofte authored a week ago
296
                    let isSelected = displayedMeterTab == tab
Bogdan Timofte authored 2 weeks ago
297

            
298
                    Button {
299
                        withAnimation(.easeInOut(duration: 0.2)) {
300
                            selectedMeterTab = tab
301
                        }
302
                    } label: {
303
                        HStack(spacing: 6) {
304
                            Image(systemName: tab.systemImage)
305
                                .font(.subheadline.weight(.semibold))
Bogdan Timofte authored a week ago
306
                            if style.showsTitles {
307
                                Text(tab.title)
308
                                    .font(.subheadline.weight(.semibold))
309
                                    .lineLimit(1)
310
                            }
Bogdan Timofte authored 2 weeks ago
311
                        }
Bogdan Timofte authored a week ago
312
                        .foregroundColor(
313
                            isSelected
314
                            ? .white
315
                            : (isFloating ? .white.opacity(0.82) : .primary)
316
                        )
317
                        .padding(.horizontal, style.chipHorizontalPadding)
318
                        .padding(.vertical, style.chipVerticalPadding)
Bogdan Timofte authored 2 weeks ago
319
                        .frame(maxWidth: .infinity)
320
                        .background(
321
                            Capsule()
Bogdan Timofte authored a week ago
322
                                .fill(
323
                                    isSelected
324
                                    ? meter.color.opacity(isFloating ? 0.94 : 1)
325
                                    : (isFloating ? Color.white.opacity(0.045) : Color.secondary.opacity(0.12))
326
                                )
Bogdan Timofte authored 2 weeks ago
327
                        )
Bogdan Timofte authored 2 weeks ago
328
                    }
Bogdan Timofte authored 2 weeks ago
329
                    .buttonStyle(.plain)
330
                    .accessibilityLabel(tab.title)
Bogdan Timofte authored 2 weeks ago
331
                }
332
            }
Bogdan Timofte authored a week ago
333
            .frame(maxWidth: style.maxWidth)
334
            .padding(style.outerPadding)
Bogdan Timofte authored 2 weeks ago
335
            .background(
Bogdan Timofte authored a week ago
336
                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
337
                    .fill(
338
                        isFloating
339
                        ? LinearGradient(
340
                            colors: [
341
                                Color.white.opacity(0.14),
342
                                Color.white.opacity(0.06)
343
                            ],
344
                            startPoint: .topLeading,
345
                            endPoint: .bottomTrailing
346
                        )
347
                        : LinearGradient(
348
                            colors: [
349
                                Color.secondary.opacity(style.barBackgroundOpacity),
350
                                Color.secondary.opacity(style.barBackgroundOpacity)
351
                            ],
352
                            startPoint: .topLeading,
353
                            endPoint: .bottomTrailing
354
                        )
355
                    )
Bogdan Timofte authored 2 weeks ago
356
            )
Bogdan Timofte authored a week ago
357
            .overlay {
358
                RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
359
                    .stroke(
360
                        isFloating ? Color.white.opacity(0.10) : Color.clear,
361
                        lineWidth: 1
362
                    )
363
            }
364
            .background {
365
                if !isFloating {
366
                    RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
367
                        .fill(.ultraThinMaterial)
368
                }
369
            }
370
            .shadow(color: Color.black.opacity(style.shadowOpacity), radius: isFloating ? 28 : 24, x: 0, y: isFloating ? 16 : 12)
Bogdan Timofte authored 2 weeks ago
371

            
372
            Spacer(minLength: 0)
373
        }
Bogdan Timofte authored a week ago
374
        .padding(.horizontal, style.horizontalPadding)
375
        .padding(.top, style.topPadding)
376
        .padding(.bottom, style.bottomPadding)
Bogdan Timofte authored 2 weeks ago
377
        .background(
Bogdan Timofte authored a week ago
378
            GeometryReader { geometry in
379
                Color.clear
380
                    .preference(key: MeterTabBarHeightPreferenceKey.self, value: geometry.size.height)
381
            }
Bogdan Timofte authored 2 weeks ago
382
        )
Bogdan Timofte authored a week ago
383
        .padding(.horizontal, style.floatingInset)
384
        .background {
385
            if style.floatingInset == 0 {
386
                Rectangle()
387
                    .fill(.ultraThinMaterial)
388
                    .opacity(style.materialOpacity)
389
                    .ignoresSafeArea(edges: .top)
390
            }
391
        }
Bogdan Timofte authored 2 weeks ago
392
        .overlay(alignment: .bottom) {
Bogdan Timofte authored a week ago
393
            if style.floatingInset == 0 {
394
                Rectangle()
395
                    .fill(Color.secondary.opacity(0.12))
396
                    .frame(height: 1)
397
            }
398
        }
399
        .overlay(alignment: .trailing) {
400
            if showsConnectionAction {
401
                MeterConnectionToolbarButton(
402
                    operationalState: meter.operationalState,
403
                    showsTitle: false,
404
                    connectAction: { meter.connect() },
405
                    disconnectAction: { meter.disconnect() }
406
                )
407
                .font(.title3.weight(.semibold))
408
                .padding(.trailing, style.horizontalPadding + style.floatingInset + 4)
409
                .padding(.top, style.topPadding)
410
                .padding(.bottom, style.bottomPadding)
411
            }
Bogdan Timofte authored 2 weeks ago
412
        }
Bogdan Timofte authored 2 weeks ago
413
    }
414

            
Bogdan Timofte authored 2 weeks ago
415
    @ViewBuilder
416
    private func landscapeSegmentedContent(size: CGSize) -> some View {
Bogdan Timofte authored a week ago
417
        switch displayedMeterTab {
Bogdan Timofte authored a week ago
418
        case .home:
419
            MeterHomeTabView(size: size, isLandscape: true)
Bogdan Timofte authored 2 weeks ago
420
        case .live:
Bogdan Timofte authored a week ago
421
            MeterLiveTabView(size: size, isLandscape: true)
Bogdan Timofte authored 2 weeks ago
422
        case .chart:
Bogdan Timofte authored a week ago
423
            MeterChartTabView(size: size, isLandscape: true)
Bogdan Timofte authored a week ago
424
        case .settings:
Bogdan Timofte authored a week ago
425
            MeterSettingsTabView(isMacIPadApp: Self.isMacIPadApp) {
426
                withAnimation(.easeInOut(duration: 0.22)) {
Bogdan Timofte authored a week ago
427
                    selectedMeterTab = .home
Bogdan Timofte authored a week ago
428
                }
429
            }
Bogdan Timofte authored 2 weeks ago
430
        }
431
    }
Bogdan Timofte authored 2 weeks ago
432

            
Bogdan Timofte authored 2 weeks ago
433
    @ViewBuilder
434
    private func portraitSegmentedContent(size: CGSize) -> some View {
Bogdan Timofte authored a week ago
435
        switch displayedMeterTab {
Bogdan Timofte authored a week ago
436
        case .home:
437
            MeterHomeTabView(size: size, isLandscape: false)
Bogdan Timofte authored 2 weeks ago
438
        case .live:
Bogdan Timofte authored a week ago
439
            MeterLiveTabView(size: size, isLandscape: false)
Bogdan Timofte authored 2 weeks ago
440
        case .chart:
Bogdan Timofte authored a week ago
441
            MeterChartTabView(size: size, isLandscape: false)
Bogdan Timofte authored a week ago
442
        case .settings:
Bogdan Timofte authored a week ago
443
            MeterSettingsTabView(isMacIPadApp: Self.isMacIPadApp) {
444
                withAnimation(.easeInOut(duration: 0.22)) {
Bogdan Timofte authored a week ago
445
                    selectedMeterTab = .home
Bogdan Timofte authored 2 weeks ago
446
                }
447
            }
Bogdan Timofte authored 2 weeks ago
448
        }
449
    }
450

            
451
    private var availableMeterTabs: [MeterTab] {
Bogdan Timofte authored a week ago
452
        var tabs: [MeterTab] = [.home]
Bogdan Timofte authored 2 weeks ago
453

            
454
        if meter.operationalState == .dataIsAvailable {
455
            tabs.append(.live)
456

            
457
            if meter.measurements.power.context.isValid {
458
                tabs.append(.chart)
459
            }
460
        }
461

            
Bogdan Timofte authored a week ago
462
        tabs.append(.settings)
463

            
Bogdan Timofte authored 2 weeks ago
464
        return tabs
465
    }
466

            
Bogdan Timofte authored a week ago
467
    private var displayedMeterTab: MeterTab {
468
        if availableMeterTabs.contains(selectedMeterTab) {
469
            return selectedMeterTab
470
        }
471
        return .home
472
    }
473

            
474
    private func restoreSelectedTab() {
475
        guard let restoredTab = MeterTab(rawValue: meter.preferredTabIdentifier) else {
476
            meter.preferredTabIdentifier = MeterTab.home.rawValue
477
            selectedMeterTab = .home
Bogdan Timofte authored 2 weeks ago
478
            return
479
        }
Bogdan Timofte authored a week ago
480

            
481
        selectedMeterTab = restoredTab
Bogdan Timofte authored 2 weeks ago
482
    }
483

            
Bogdan Timofte authored 2 weeks ago
484
    private var meterBackground: some View {
485
        LinearGradient(
486
            colors: [
487
                meter.color.opacity(0.22),
488
                Color.secondary.opacity(0.08),
489
                Color.clear
490
            ],
491
            startPoint: .topLeading,
492
            endPoint: .bottomTrailing
493
        )
494
        .ignoresSafeArea()
495
    }
496

            
497
    private func isLandscape(size: CGSize) -> Bool {
498
        size.width > size.height
499
    }
500

            
Bogdan Timofte authored a week ago
501
    private func tabBarStyle(for landscape: Bool, usesOverlayTabBar: Bool) -> TabBarStyle {
502
        if usesOverlayTabBar {
503
            return .landscapeFloating
504
        }
505

            
506
        if landscape {
507
            return .landscapeInline
508
        }
509

            
510
        return .portrait
511
    }
512

            
513
    private func landscapeContentTopPadding(for style: TabBarStyle) -> CGFloat {
514
        if style.floatingInset > 0 {
515
            return max(landscapeTabBarHeight * 0.44, 26)
516
        }
517

            
518
        return max(landscapeTabBarHeight - 6, 0)
519
    }
520

            
521
}
522

            
523
private struct MeterTabBarHeightPreferenceKey: PreferenceKey {
524
    static var defaultValue: CGFloat = 0
525

            
526
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
527
        value = max(value, nextValue())
528
    }
Bogdan Timofte authored a week ago
529
}
530

            
Bogdan Timofte authored 2 weeks ago
531
// MARK: - Conditional navigation bar modifier (skipped on Designed-for-iPad / Mac)
532

            
533
private struct IOSOnlyNavBar: ViewModifier {
534
    let apply: Bool
535
    let title: String
536
    let showRSSI: Bool
537
    let rssi: Int
538
    let meter: Meter
539

            
540
    @ViewBuilder
541
    func body(content: Content) -> some View {
542
        if apply {
543
            content
544
                .navigationBarTitle(title)
545
                .toolbar {
546
                    ToolbarItemGroup(placement: .navigationBarTrailing) {
Bogdan Timofte authored a week ago
547
                        MeterConnectionToolbarButton(
548
                            operationalState: meter.operationalState,
549
                            showsTitle: false,
550
                            connectAction: { meter.connect() },
551
                            disconnectAction: { meter.disconnect() }
552
                        )
553
                        .font(.body.weight(.semibold))
Bogdan Timofte authored 2 weeks ago
554
                        if showRSSI {
555
                            RSSIView(RSSI: rssi)
556
                                .frame(width: 18, height: 18)
557
                        }
558
                    }
559
                }
560
        } else {
561
            content
562
        }
563
    }
564
}