Newer Older
576 lines | 20.044kb
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

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

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

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

            
Bogdan Timofte authored a week ago
89
    private enum MeterTab: String, Hashable {
Bogdan Timofte authored a week ago
90
        case home
Bogdan Timofte authored 2 weeks ago
91
        case live
92
        case chart
Bogdan Timofte authored a week ago
93
        case settings
Bogdan Timofte authored 2 weeks ago
94

            
95
        var title: String {
96
            switch self {
Bogdan Timofte authored a week ago
97
            case .home: return "Home"
Bogdan Timofte authored 2 weeks ago
98
            case .live: return "Live"
99
            case .chart: return "Chart"
Bogdan Timofte authored a week ago
100
            case .settings: return "Settings"
Bogdan Timofte authored 2 weeks ago
101
            }
102
        }
103

            
104
        var systemImage: String {
105
            switch self {
Bogdan Timofte authored a week ago
106
            case .home: return "house.fill"
Bogdan Timofte authored 2 weeks ago
107
            case .live: return "waveform.path.ecg"
108
            case .chart: return "chart.xyaxis.line"
Bogdan Timofte authored a week ago
109
            case .settings: return "gearshape.fill"
Bogdan Timofte authored 2 weeks ago
110
            }
111
        }
112
    }
Bogdan Timofte authored 2 weeks ago
113

            
114
    @EnvironmentObject private var meter: Meter
Bogdan Timofte authored 2 weeks ago
115
    @Environment(\.dismiss) private var dismiss
116

            
117
    private static let isMacIPadApp: Bool = ProcessInfo.processInfo.isiOSAppOnMac
Bogdan Timofte authored a week ago
118
    #if os(iOS)
119
    private static let isPhone: Bool = UIDevice.current.userInterfaceIdiom == .phone
120
    #else
121
    private static let isPhone: Bool = false
122
    #endif
Bogdan Timofte authored 2 weeks ago
123

            
Bogdan Timofte authored a week ago
124
    @State private var selectedMeterTab: MeterTab = .home
Bogdan Timofte authored 2 weeks ago
125
    @State private var navBarTitle: String = "Meter"
126
    @State private var navBarShowRSSI: Bool = false
127
    @State private var navBarRSSI: Int = 0
Bogdan Timofte authored a week ago
128
    @State private var landscapeTabBarHeight: CGFloat = 0
Bogdan Timofte authored 2 weeks ago
129

            
Bogdan Timofte authored 2 weeks ago
130
    var body: some View {
Bogdan Timofte authored 2 weeks ago
131
        GeometryReader { proxy in
132
            let landscape = isLandscape(size: proxy.size)
Bogdan Timofte authored a week ago
133
            let usesOverlayTabBar = landscape && Self.isPhone
Bogdan Timofte authored a week ago
134
            let tabBarStyle = tabBarStyle(
135
                for: landscape,
136
                usesOverlayTabBar: usesOverlayTabBar,
137
                size: proxy.size
138
            )
Bogdan Timofte authored 2 weeks ago
139

            
Bogdan Timofte authored 2 weeks ago
140
            VStack(spacing: 0) {
141
                if Self.isMacIPadApp {
142
                    macNavigationHeader
143
                }
144
                Group {
145
                    if landscape {
Bogdan Timofte authored a week ago
146
                        landscapeDeck(
147
                            size: proxy.size,
148
                            usesOverlayTabBar: usesOverlayTabBar,
149
                            tabBarStyle: tabBarStyle
150
                        )
Bogdan Timofte authored 2 weeks ago
151
                    } else {
Bogdan Timofte authored a week ago
152
                        portraitContent(size: proxy.size, tabBarStyle: tabBarStyle)
Bogdan Timofte authored 2 weeks ago
153
                    }
Bogdan Timofte authored 2 weeks ago
154
                }
Bogdan Timofte authored 2 weeks ago
155
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
Bogdan Timofte authored 2 weeks ago
156
            }
Bogdan Timofte authored 2 weeks ago
157
            #if !targetEnvironment(macCatalyst)
Bogdan Timofte authored 2 weeks ago
158
            .navigationBarHidden(Self.isMacIPadApp || landscape)
Bogdan Timofte authored 2 weeks ago
159
            #endif
Bogdan Timofte authored 2 weeks ago
160
        }
161
        .background(meterBackground)
Bogdan Timofte authored 2 weeks ago
162
        .modifier(IOSOnlyNavBar(
163
            apply: !Self.isMacIPadApp,
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 a week ago
185
        .onChange(of: selectedMeterTab) { newTab in
186
            meter.preferredTabIdentifier = newTab.rawValue
187
        }
Bogdan Timofte authored 2 weeks 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 a week ago
212
            MeterConnectionToolbarButton(
213
                operationalState: meter.operationalState,
214
                showsTitle: true,
215
                connectAction: { meter.connect() },
216
                disconnectAction: { meter.disconnect() }
217
            )
218

            
Bogdan Timofte authored 2 weeks ago
219
            if meter.operationalState > .notPresent {
Bogdan Timofte authored 2 weeks ago
220
                RSSIView(RSSI: meter.btSerial.averageRSSI)
Bogdan Timofte authored 2 weeks ago
221
                    .frame(width: 18, height: 18)
222
            }
Bogdan Timofte authored 2 weeks 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 weeks ago
237
    }
238

            
Bogdan Timofte authored a week ago
239
    private func portraitContent(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
240
        portraitSegmentedDeck(size: size, tabBarStyle: tabBarStyle)
241
    }
242

            
243
    @ViewBuilder
244
    private func landscapeDeck(size: CGSize, usesOverlayTabBar: Bool, tabBarStyle: TabBarStyle) -> some View {
245
        if usesOverlayTabBar {
246
            landscapeOverlaySegmentedDeck(size: size, tabBarStyle: tabBarStyle)
247
        } else {
248
            landscapeSegmentedDeck(size: size, tabBarStyle: tabBarStyle)
249
        }
Bogdan Timofte authored 2 weeks ago
250
    }
251

            
Bogdan Timofte authored a week ago
252
    private func landscapeOverlaySegmentedDeck(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
253
        ZStack(alignment: .top) {
254
            landscapeSegmentedContent(size: size)
255
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
256
                .padding(.top, landscapeContentTopPadding(for: tabBarStyle))
Bogdan Timofte authored a week ago
257
                .id(displayedMeterTab)
Bogdan Timofte authored a week ago
258
                .transition(.opacity.combined(with: .move(edge: .trailing)))
259

            
260
            segmentedTabBar(style: tabBarStyle, showsConnectionAction: !Self.isMacIPadApp)
261
        }
Bogdan Timofte authored a week ago
262
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored a week ago
263
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
264
        .onAppear {
Bogdan Timofte authored a week ago
265
            restoreSelectedTab()
Bogdan Timofte authored a week ago
266
        }
267
        .onPreferenceChange(MeterTabBarHeightPreferenceKey.self) { height in
268
            if height > 0 {
269
                landscapeTabBarHeight = height
270
            }
271
        }
Bogdan Timofte authored 2 weeks ago
272
    }
273

            
Bogdan Timofte authored a week ago
274
    private func landscapeSegmentedDeck(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, showsConnectionAction: !Self.isMacIPadApp)
Bogdan Timofte authored 2 weeks ago
277

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

            
Bogdan Timofte authored a week ago
290
    private func portraitSegmentedDeck(size: CGSize, tabBarStyle: TabBarStyle) -> some View {
Bogdan Timofte authored 2 weeks ago
291
        VStack(spacing: 0) {
Bogdan Timofte authored a week ago
292
            segmentedTabBar(style: tabBarStyle)
Bogdan Timofte authored 2 weeks ago
293

            
294
            portraitSegmentedContent(size: size)
295
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
Bogdan Timofte authored a week ago
296
                .id(displayedMeterTab)
Bogdan Timofte authored 2 weeks ago
297
                .transition(.opacity.combined(with: .move(edge: .trailing)))
298
        }
Bogdan Timofte authored a week ago
299
        .animation(.easeInOut(duration: 0.22), value: displayedMeterTab)
Bogdan Timofte authored 2 weeks ago
300
        .animation(.easeInOut(duration: 0.22), value: availableMeterTabs)
301
        .onAppear {
Bogdan Timofte authored a week ago
302
            restoreSelectedTab()
Bogdan Timofte authored 2 weeks ago
303
        }
304
    }
305

            
Bogdan Timofte authored a week ago
306
    private func segmentedTabBar(style: TabBarStyle, showsConnectionAction: Bool = false) -> some View {
307
        let isFloating = style.floatingInset > 0
308
        let cornerRadius = style.showsTitles ? 14.0 : 22.0
Bogdan Timofte authored a week ago
309
        let unselectedForegroundColor = isFloating ? Color.primary.opacity(0.86) : Color.primary
310
        let unselectedChipFill = isFloating ? Color.black.opacity(0.08) : Color.secondary.opacity(0.12)
Bogdan Timofte authored a week ago
311

            
312
        return HStack {
Bogdan Timofte authored 2 weeks ago
313
            Spacer(minLength: 0)
314

            
315
            HStack(spacing: 8) {
316
                ForEach(availableMeterTabs, id: \.self) { tab in
Bogdan Timofte authored a week ago
317
                    let isSelected = displayedMeterTab == tab
Bogdan Timofte authored 2 weeks ago
318

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

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

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

            
Bogdan Timofte authored 2 weeks ago
453
    @ViewBuilder
454
    private func portraitSegmentedContent(size: CGSize) -> some View {
Bogdan Timofte authored a week ago
455
        switch displayedMeterTab {
Bogdan Timofte authored a week ago
456
        case .home:
457
            MeterHomeTabView(size: size, isLandscape: false)
Bogdan Timofte authored 2 weeks ago
458
        case .live:
Bogdan Timofte authored a week ago
459
            MeterLiveTabView(size: size, isLandscape: false)
Bogdan Timofte authored 2 weeks ago
460
        case .chart:
Bogdan Timofte authored a week ago
461
            MeterChartTabView(size: size, isLandscape: false)
Bogdan Timofte authored a week ago
462
        case .settings:
Bogdan Timofte authored a week ago
463
            MeterSettingsTabView(isMacIPadApp: Self.isMacIPadApp) {
464
                withAnimation(.easeInOut(duration: 0.22)) {
Bogdan Timofte authored a week ago
465
                    selectedMeterTab = .home
Bogdan Timofte authored 2 weeks ago
466
                }
467
            }
Bogdan Timofte authored 2 weeks ago
468
        }
469
    }
470

            
471
    private var availableMeterTabs: [MeterTab] {
Bogdan Timofte authored a week ago
472
        [.home, .live, .chart, .settings]
Bogdan Timofte authored 2 weeks ago
473
    }
474

            
Bogdan Timofte authored a week ago
475
    private var displayedMeterTab: MeterTab {
476
        if availableMeterTabs.contains(selectedMeterTab) {
477
            return selectedMeterTab
478
        }
479
        return .home
480
    }
481

            
482
    private func restoreSelectedTab() {
483
        guard let restoredTab = MeterTab(rawValue: meter.preferredTabIdentifier) else {
484
            meter.preferredTabIdentifier = MeterTab.home.rawValue
485
            selectedMeterTab = .home
Bogdan Timofte authored 2 weeks ago
486
            return
487
        }
Bogdan Timofte authored a week ago
488

            
489
        selectedMeterTab = restoredTab
Bogdan Timofte authored 2 weeks ago
490
    }
491

            
Bogdan Timofte authored 2 weeks ago
492
    private var meterBackground: some View {
493
        LinearGradient(
494
            colors: [
495
                meter.color.opacity(0.22),
496
                Color.secondary.opacity(0.08),
497
                Color.clear
498
            ],
499
            startPoint: .topLeading,
500
            endPoint: .bottomTrailing
501
        )
502
        .ignoresSafeArea()
503
    }
504

            
505
    private func isLandscape(size: CGSize) -> Bool {
506
        size.width > size.height
507
    }
508

            
Bogdan Timofte authored a week ago
509
    private func tabBarStyle(for landscape: Bool, usesOverlayTabBar: Bool, size: CGSize) -> TabBarStyle {
Bogdan Timofte authored a week ago
510
        if usesOverlayTabBar {
511
            return .landscapeFloating
512
        }
513

            
514
        if landscape {
515
            return .landscapeInline
516
        }
517

            
Bogdan Timofte authored a week ago
518
        if Self.isPhone && size.width < 390 {
519
            return .portraitCompact
520
        }
521

            
Bogdan Timofte authored a week ago
522
        return .portrait
523
    }
524

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

            
530
        return max(landscapeTabBarHeight - 6, 0)
531
    }
532

            
533
}
534

            
535
private struct MeterTabBarHeightPreferenceKey: PreferenceKey {
536
    static var defaultValue: CGFloat = 0
537

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

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

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

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