Newer Older
578 lines | 20.203kb
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 2 weeks ago
74
    private enum MeterTab: 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
        }
166
    }
167

            
168
    // MARK: - Custom navigation header for Designed-for-iPad on Mac
169

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

            
184
            Text(meter.name.isEmpty ? "Meter" : meter.name)
185
                .font(.headline)
186
                .lineLimit(1)
187

            
188
            Spacer()
189

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

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

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

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

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

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

            
238
            segmentedTabBar(style: tabBarStyle, showsConnectionAction: !Self.isMacIPadApp)
239
        }
240
        .animation(.easeInOut(duration: 0.22), value: selectedMeterTab)
241
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
242
        .onAppear {
243
            normalizeSelectedTab()
244
        }
245
        .onChange(of: availableMeterTabs) { _ in
246
            normalizeSelectedTab()
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)
261
                .id(selectedMeterTab)
262
                .transition(.opacity.combined(with: .move(edge: .trailing)))
263
        }
264
        .animation(.easeInOut(duration: 0.22), value: selectedMeterTab)
265
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
266
        .onAppear {
267
            normalizeSelectedTab()
268
        }
269
        .onChange(of: availableMeterTabs) { _ in
270
            normalizeSelectedTab()
271
        }
272
    }
273

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

            
278
            portraitSegmentedContent(size: size)
279
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
280
                .id(selectedMeterTab)
281
                .transition(.opacity.combined(with: .move(edge: .trailing)))
282
        }
283
        .animation(.easeInOut(duration: 0.22), value: selectedMeterTab)
284
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
285
        .onAppear {
286
            normalizeSelectedTab()
287
        }
288
        .onChange(of: availableMeterTabs) { _ in
289
            normalizeSelectedTab()
290
        }
291
    }
292

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

            
297
        return HStack {
Bogdan Timofte authored 2 weeks ago
298
            Spacer(minLength: 0)
299

            
300
            HStack(spacing: 8) {
301
                ForEach(availableMeterTabs, id: \.self) { tab in
302
                    let isSelected = selectedMeterTab == tab
303

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

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

            
Bogdan Timofte authored 2 weeks ago
421
    @ViewBuilder
422
    private func landscapeSegmentedContent(size: CGSize) -> some View {
423
        switch selectedMeterTab {
Bogdan Timofte authored a week ago
424
        case .home:
425
            MeterHomeTabView(size: size, isLandscape: true)
Bogdan Timofte authored 2 weeks ago
426
        case .live:
427
            if meter.operationalState == .dataIsAvailable {
Bogdan Timofte authored a week ago
428
                MeterLiveTabView(size: size, isLandscape: true)
Bogdan Timofte authored 2 weeks ago
429
            } else {
Bogdan Timofte authored a week ago
430
                MeterHomeTabView(size: size, isLandscape: true)
Bogdan Timofte authored 2 weeks ago
431
            }
Bogdan Timofte authored 2 weeks ago
432
        case .chart:
433
            if meter.measurements.power.context.isValid && meter.operationalState == .dataIsAvailable {
Bogdan Timofte authored a week ago
434
                MeterChartTabView(size: size, isLandscape: true)
Bogdan Timofte authored 2 weeks ago
435
            } else {
Bogdan Timofte authored a week ago
436
                MeterHomeTabView(size: size, isLandscape: true)
Bogdan Timofte authored 2 weeks ago
437
            }
Bogdan Timofte authored a week ago
438
        case .settings:
Bogdan Timofte authored a week ago
439
            MeterSettingsTabView(isMacIPadApp: Self.isMacIPadApp) {
440
                withAnimation(.easeInOut(duration: 0.22)) {
Bogdan Timofte authored a week ago
441
                    selectedMeterTab = .home
Bogdan Timofte authored a week ago
442
                }
443
            }
Bogdan Timofte authored 2 weeks ago
444
        }
445
    }
Bogdan Timofte authored 2 weeks ago
446

            
Bogdan Timofte authored 2 weeks ago
447
    @ViewBuilder
448
    private func portraitSegmentedContent(size: CGSize) -> some View {
449
        switch selectedMeterTab {
Bogdan Timofte authored a week ago
450
        case .home:
451
            MeterHomeTabView(size: size, isLandscape: false)
Bogdan Timofte authored 2 weeks ago
452
        case .live:
Bogdan Timofte authored 2 weeks ago
453
            if meter.operationalState == .dataIsAvailable {
Bogdan Timofte authored a week ago
454
                MeterLiveTabView(size: size, isLandscape: false)
Bogdan Timofte authored 2 weeks ago
455
            } else {
Bogdan Timofte authored a week ago
456
                MeterHomeTabView(size: size, isLandscape: false)
Bogdan Timofte authored 2 weeks ago
457
            }
458
        case .chart:
459
            if meter.measurements.power.context.isValid && meter.operationalState == .dataIsAvailable {
Bogdan Timofte authored a week ago
460
                MeterChartTabView(size: size, isLandscape: false)
Bogdan Timofte authored 2 weeks ago
461
            } else {
Bogdan Timofte authored a week ago
462
                MeterHomeTabView(size: size, isLandscape: false)
Bogdan Timofte authored 2 weeks ago
463
            }
Bogdan Timofte authored a week ago
464
        case .settings:
Bogdan Timofte authored a week ago
465
            MeterSettingsTabView(isMacIPadApp: Self.isMacIPadApp) {
466
                withAnimation(.easeInOut(duration: 0.22)) {
Bogdan Timofte authored a week ago
467
                    selectedMeterTab = .home
Bogdan Timofte authored 2 weeks ago
468
                }
469
            }
Bogdan Timofte authored 2 weeks ago
470
        }
471
    }
472

            
473
    private var availableMeterTabs: [MeterTab] {
Bogdan Timofte authored a week ago
474
        var tabs: [MeterTab] = [.home]
Bogdan Timofte authored 2 weeks ago
475

            
476
        if meter.operationalState == .dataIsAvailable {
477
            tabs.append(.live)
478

            
479
            if meter.measurements.power.context.isValid {
480
                tabs.append(.chart)
481
            }
482
        }
483

            
Bogdan Timofte authored a week ago
484
        tabs.append(.settings)
485

            
Bogdan Timofte authored 2 weeks ago
486
        return tabs
487
    }
488

            
489
    private func normalizeSelectedTab() {
490
        guard availableMeterTabs.contains(selectedMeterTab) else {
491
            withAnimation(.easeInOut(duration: 0.22)) {
Bogdan Timofte authored a week ago
492
                selectedMeterTab = .home
Bogdan Timofte authored 2 weeks ago
493
            }
494
            return
495
        }
496
    }
497

            
Bogdan Timofte authored 2 weeks ago
498
    private var meterBackground: some View {
499
        LinearGradient(
500
            colors: [
501
                meter.color.opacity(0.22),
502
                Color.secondary.opacity(0.08),
503
                Color.clear
504
            ],
505
            startPoint: .topLeading,
506
            endPoint: .bottomTrailing
507
        )
508
        .ignoresSafeArea()
509
    }
510

            
511
    private func isLandscape(size: CGSize) -> Bool {
512
        size.width > size.height
513
    }
514

            
Bogdan Timofte authored a week ago
515
    private func tabBarStyle(for landscape: Bool, usesOverlayTabBar: Bool) -> TabBarStyle {
516
        if usesOverlayTabBar {
517
            return .landscapeFloating
518
        }
519

            
520
        if landscape {
521
            return .landscapeInline
522
        }
523

            
524
        return .portrait
525
    }
526

            
527
    private func landscapeContentTopPadding(for style: TabBarStyle) -> CGFloat {
528
        if style.floatingInset > 0 {
529
            return max(landscapeTabBarHeight * 0.44, 26)
530
        }
531

            
532
        return max(landscapeTabBarHeight - 6, 0)
533
    }
534

            
535
}
536

            
537
private struct MeterTabBarHeightPreferenceKey: PreferenceKey {
538
    static var defaultValue: CGFloat = 0
539

            
540
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
541
        value = max(value, nextValue())
542
    }
Bogdan Timofte authored a week ago
543
}
544

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

            
547
private struct IOSOnlyNavBar: ViewModifier {
548
    let apply: Bool
549
    let title: String
550
    let showRSSI: Bool
551
    let rssi: Int
552
    let meter: Meter
553

            
554
    @ViewBuilder
555
    func body(content: Content) -> some View {
556
        if apply {
557
            content
558
                .navigationBarTitle(title)
559
                .toolbar {
560
                    ToolbarItemGroup(placement: .navigationBarTrailing) {
Bogdan Timofte authored a week ago
561
                        MeterConnectionToolbarButton(
562
                            operationalState: meter.operationalState,
563
                            showsTitle: false,
564
                            connectAction: { meter.connect() },
565
                            disconnectAction: { meter.disconnect() }
566
                        )
567
                        .font(.body.weight(.semibold))
Bogdan Timofte authored 2 weeks ago
568
                        if showRSSI {
569
                            RSSIView(RSSI: rssi)
570
                                .frame(width: 18, height: 18)
571
                        }
572
                    }
573
                }
574
        } else {
575
            content
576
        }
577
    }
578
}