Skip to content

Commit 3349999

Browse files
authored
Merge pull request #437 from loopandlearn/more-menu
More menu
2 parents e1c75da + c680029 commit 3349999

File tree

9 files changed

+635
-52
lines changed

9 files changed

+635
-52
lines changed

LoopFollow.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
DD16AF112C997B4600FB655A /* LoadingButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF102C997B4600FB655A /* LoadingButtonView.swift */; };
4242
DD1A97142D4294A5000DDC11 /* AdvancedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A97132D4294A4000DDC11 /* AdvancedSettingsView.swift */; };
4343
DD1A97162D4294B3000DDC11 /* AdvancedSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A97152D4294B2000DDC11 /* AdvancedSettingsViewModel.swift */; };
44+
DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52B82E1EB5DC00432050 /* TabPosition.swift */; };
45+
DD1D52BB2E1EB60B00432050 /* MoreMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */; };
4446
DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */; };
4547
DD2C2E512D3B8B0C006413A5 /* NightscoutSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */; };
4648
DD2C2E542D3C37DC006413A5 /* DexcomSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */; };
@@ -121,6 +123,7 @@
121123
DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */; };
122124
DD7F4C252DD7B20700D449E9 /* AlarmType+timeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */; };
123125
DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; };
126+
DD8060DB2E2ACE5900626B91 /* TabCustomizationModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8060DA2E2ACE5900626B91 /* TabCustomizationModal.swift */; };
124127
DD8316182DE3633D004467AA /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316172DE3633D004467AA /* GeneralSettingsView.swift */; };
125128
DD8316442DE47CA9004467AA /* BGPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316432DE47CA9004467AA /* BGPicker.swift */; };
126129
DD8316462DE49B09004467AA /* GraphSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316452DE49B09004467AA /* GraphSettingsView.swift */; };
@@ -415,6 +418,8 @@
415418
DD16AF102C997B4600FB655A /* LoadingButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonView.swift; sourceTree = "<group>"; };
416419
DD1A97132D4294A4000DDC11 /* AdvancedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsView.swift; sourceTree = "<group>"; };
417420
DD1A97152D4294B2000DDC11 /* AdvancedSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsViewModel.swift; sourceTree = "<group>"; };
421+
DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = "<group>"; };
422+
DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuViewController.swift; sourceTree = "<group>"; };
418423
DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsView.swift; sourceTree = "<group>"; };
419424
DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsViewModel.swift; sourceTree = "<group>"; };
420425
DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSettingsViewModel.swift; sourceTree = "<group>"; };
@@ -494,6 +499,7 @@
494499
DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+SortDirection.swift"; sourceTree = "<group>"; };
495500
DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+timeUnit.swift"; sourceTree = "<group>"; };
496501
DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = "<group>"; };
502+
DD8060DA2E2ACE5900626B91 /* TabCustomizationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationModal.swift; sourceTree = "<group>"; };
497503
DD8316172DE3633D004467AA /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = "<group>"; };
498504
DD8316432DE47CA9004467AA /* BGPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGPicker.swift; sourceTree = "<group>"; };
499505
DD8316452DE49B09004467AA /* GraphSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsView.swift; sourceTree = "<group>"; };
@@ -863,6 +869,7 @@
863869
DD1A97122D429495000DDC11 /* Settings */ = {
864870
isa = PBXGroup;
865871
children = (
872+
DD8060DA2E2ACE5900626B91 /* TabCustomizationModal.swift */,
866873
DD83164F2DE4E635004467AA /* SettingsMenuView.swift */,
867874
DD2C2E552D3C3913006413A5 /* DexcomSettingsView.swift */,
868875
DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */,
@@ -1456,6 +1463,7 @@
14561463
FCC688542489367300A0279D /* Helpers */ = {
14571464
isa = PBXGroup;
14581465
children = (
1466+
DD1D52B82E1EB5DC00432050 /* TabPosition.swift */,
14591467
DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */,
14601468
DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */,
14611469
DD7B0D432D730A320063DCB6 /* CycleHelper.swift */,
@@ -1486,6 +1494,7 @@
14861494
FCC68871248A736700A0279D /* ViewControllers */ = {
14871495
isa = PBXGroup;
14881496
children = (
1497+
DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */,
14891498
DD12D4842E1705D9004E0112 /* AlarmViewController.swift */,
14901499
FC97881B2485969B00A7906C /* MainViewController.swift */,
14911500
FC97881D2485969B00A7906C /* NightScoutViewController.swift */,
@@ -1825,6 +1834,7 @@
18251834
DD0B9D582DE1F3B20090C337 /* AlarmType+canAcknowledge.swift in Sources */,
18261835
DD2C2E562D3C3917006413A5 /* DexcomSettingsView.swift in Sources */,
18271836
DD7F4BC52DD3CE0700D449E9 /* AlarmBGLimitSection.swift in Sources */,
1837+
DD8060DB2E2ACE5900626B91 /* TabCustomizationModal.swift in Sources */,
18281838
DD7F4B9F2DD1F92700D449E9 /* AlarmActiveSection.swift in Sources */,
18291839
DD4AFB672DB68C5500BB593F /* UUID+Identifiable.swift in Sources */,
18301840
DD9ED0CA2D355257000D2A63 /* LogView.swift in Sources */,
@@ -1938,6 +1948,7 @@
19381948
FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */,
19391949
FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */,
19401950
DDF6999E2C5AAA640058A8D9 /* ErrorMessageView.swift in Sources */,
1951+
DD1D52BB2E1EB60B00432050 /* MoreMenuViewController.swift in Sources */,
19411952
DD4878152C7B75230048F05C /* MealView.swift in Sources */,
19421953
FC16A97F249969E2003D6245 /* Graphs.swift in Sources */,
19431954
FC8589BF252B54F500C8FC73 /* Mobileprovision.swift in Sources */,
@@ -2034,6 +2045,7 @@
20342045
DDC6CA412DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift in Sources */,
20352046
DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */,
20362047
DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */,
2048+
DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */,
20372049
DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */,
20382050
DD0650F52DCF303F004D3B41 /* AlarmStepperSection.swift in Sources */,
20392051
DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */,

LoopFollow/Application/Base.lproj/Main.storyboard

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@
321321
<!--Snoozer-->
322322
<scene sceneID="gSd-1V-vbL">
323323
<objects>
324-
<viewController id="e0E-cx-0Wr" customClass="SnoozerViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
324+
<viewController storyboardIdentifier="SnoozerViewController" id="e0E-cx-0Wr" customClass="SnoozerViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
325325
<view key="view" contentMode="scaleToFill" id="oqe-Oz-Hai">
326326
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
327327
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -337,7 +337,7 @@
337337
<!--Nightscout-->
338338
<scene sceneID="wg7-f3-ORb">
339339
<objects>
340-
<viewController id="8rJ-Kc-sve" customClass="NightscoutViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
340+
<viewController storyboardIdentifier="NightscoutViewController" id="8rJ-Kc-sve" customClass="NightscoutViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
341341
<view key="view" contentMode="scaleToFill" id="QS5-Rx-YEW">
342342
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
343343
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -405,7 +405,7 @@
405405
<!--Settings-->
406406
<scene sceneID="ORV-wr-Fd3">
407407
<objects>
408-
<viewController id="hDv-qK-Udb" customClass="SettingsViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
408+
<viewController storyboardIdentifier="SettingsViewController" id="hDv-qK-Udb" customClass="SettingsViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
409409
<view key="view" contentMode="scaleToFill" id="ljg-dW-5uR">
410410
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
411411
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>

LoopFollow/Helpers/TabPosition.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// LoopFollow
2+
// TabPosition.swift
3+
// Created by Jonas Björkert.
4+
5+
enum TabPosition: String, CaseIterable, Codable {
6+
case position2
7+
case position4
8+
case more
9+
case disabled
10+
11+
var displayName: String {
12+
switch self {
13+
case .position2: return "Tab 2"
14+
case .position4: return "Tab 4"
15+
case .more: return "More Menu"
16+
case .disabled: return "Hidden"
17+
}
18+
}
19+
}

LoopFollow/Settings/SettingsMenuView.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct SettingsMenuView: View {
1515

1616
@State private var latestVersion: String?
1717
@State private var versionTint: Color = .secondary
18+
@State private var showingTabCustomization = false
1819

1920
// MARK: – Body
2021

@@ -44,6 +45,12 @@ struct SettingsMenuView: View {
4445
settingsPath.value.append(Sheet.graph)
4546
}
4647

48+
NavigationRow(title: "Tab Settings",
49+
icon: "rectangle.3.group")
50+
{
51+
showingTabCustomization = true
52+
}
53+
4754
if !nightscoutURL.value.isEmpty {
4855
NavigationRow(title: "Information Display Settings",
4956
icon: "info.circle")
@@ -122,6 +129,15 @@ struct SettingsMenuView: View {
122129
}
123130
.navigationTitle("Settings")
124131
.navigationDestination(for: Sheet.self) { $0.destination }
132+
.sheet(isPresented: $showingTabCustomization) {
133+
TabCustomizationModal(
134+
isPresented: $showingTabCustomization,
135+
onApply: {
136+
// Dismiss any presented view controller and go to home tab
137+
handleTabReorganization()
138+
}
139+
)
140+
}
125141
}
126142
.task { await refreshVersionInfo() }
127143
}
@@ -212,6 +228,37 @@ struct SettingsMenuView: View {
212228
applicationActivities: nil)
213229
UIApplication.shared.topMost?.present(avc, animated: true)
214230
}
231+
232+
private func handleTabReorganization() {
233+
// Find the root tab bar controller
234+
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
235+
let window = windowScene.windows.first,
236+
let rootVC = window.rootViewController else { return }
237+
238+
// Navigate through the hierarchy to find the tab bar controller
239+
var tabBarController: UITabBarController?
240+
241+
if let tbc = rootVC as? UITabBarController {
242+
tabBarController = tbc
243+
} else if let nav = rootVC as? UINavigationController,
244+
let tbc = nav.viewControllers.first as? UITabBarController
245+
{
246+
tabBarController = tbc
247+
}
248+
249+
guard let tabBar = tabBarController else { return }
250+
251+
// Dismiss any modals first
252+
if let presented = tabBar.presentedViewController {
253+
presented.dismiss(animated: false) {
254+
// After dismissal, switch to home tab
255+
tabBar.selectedIndex = 0
256+
}
257+
} else {
258+
// No modal to dismiss, just switch to home
259+
tabBar.selectedIndex = 0
260+
}
261+
}
215262
}
216263

217264
// MARK: – Sheet routing
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// LoopFollow
2+
// TabCustomizationModal.swift
3+
// Created by Jonas Björkert.
4+
5+
import SwiftUI
6+
7+
struct TabCustomizationModal: View {
8+
@Binding var isPresented: Bool
9+
let onApply: () -> Void
10+
11+
// Local state for editing
12+
@State private var alarmsPosition: TabPosition
13+
@State private var remotePosition: TabPosition
14+
@State private var nightscoutPosition: TabPosition
15+
@State private var hasChanges = false
16+
17+
// Store original values to detect changes
18+
private let originalAlarmsPosition: TabPosition
19+
private let originalRemotePosition: TabPosition
20+
private let originalNightscoutPosition: TabPosition
21+
22+
init(isPresented: Binding<Bool>, onApply: @escaping () -> Void) {
23+
_isPresented = isPresented
24+
self.onApply = onApply
25+
26+
// Initialize with current values
27+
let currentAlarms = Storage.shared.alarmsPosition.value
28+
let currentRemote = Storage.shared.remotePosition.value
29+
let currentNightscout = Storage.shared.nightscoutPosition.value
30+
31+
_alarmsPosition = State(initialValue: currentAlarms)
32+
_remotePosition = State(initialValue: currentRemote)
33+
_nightscoutPosition = State(initialValue: currentNightscout)
34+
35+
originalAlarmsPosition = currentAlarms
36+
originalRemotePosition = currentRemote
37+
originalNightscoutPosition = currentNightscout
38+
}
39+
40+
var body: some View {
41+
NavigationView {
42+
Form {
43+
Section("Tab Positions") {
44+
TabPositionRow(
45+
title: "Alarms",
46+
icon: "alarm",
47+
position: $alarmsPosition,
48+
otherPositions: [remotePosition, nightscoutPosition]
49+
)
50+
.onChange(of: alarmsPosition) { _ in checkForChanges() }
51+
52+
TabPositionRow(
53+
title: "Remote",
54+
icon: "antenna.radiowaves.left.and.right",
55+
position: $remotePosition,
56+
otherPositions: [alarmsPosition, nightscoutPosition]
57+
)
58+
.onChange(of: remotePosition) { _ in checkForChanges() }
59+
60+
TabPositionRow(
61+
title: "Nightscout",
62+
icon: "safari",
63+
position: $nightscoutPosition,
64+
otherPositions: [alarmsPosition, remotePosition]
65+
)
66+
.onChange(of: nightscoutPosition) { _ in checkForChanges() }
67+
}
68+
69+
Section {
70+
Text("• Tab 2 and Tab 4 can each hold one item")
71+
.font(.caption)
72+
.foregroundColor(.secondary)
73+
Text("• Items in 'More Menu' appear under the last tab")
74+
.font(.caption)
75+
.foregroundColor(.secondary)
76+
Text("• Hidden items are not accessible")
77+
.font(.caption)
78+
.foregroundColor(.secondary)
79+
}
80+
81+
if hasChanges {
82+
Section {
83+
Text("Changes will be applied when you tap 'Apply'")
84+
.font(.caption)
85+
.foregroundColor(.orange)
86+
}
87+
}
88+
}
89+
.navigationTitle("Tab Settings")
90+
.navigationBarTitleDisplayMode(.inline)
91+
.toolbar {
92+
ToolbarItem(placement: .navigationBarLeading) {
93+
Button("Cancel") {
94+
isPresented = false
95+
}
96+
}
97+
98+
ToolbarItem(placement: .navigationBarTrailing) {
99+
Button("Apply") {
100+
applyChanges()
101+
}
102+
.fontWeight(.semibold)
103+
.disabled(!hasChanges)
104+
}
105+
}
106+
}
107+
.preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil)
108+
}
109+
110+
private func checkForChanges() {
111+
hasChanges = alarmsPosition != originalAlarmsPosition ||
112+
remotePosition != originalRemotePosition ||
113+
nightscoutPosition != originalNightscoutPosition
114+
}
115+
116+
private func applyChanges() {
117+
// Save the new positions
118+
Storage.shared.alarmsPosition.value = alarmsPosition
119+
Storage.shared.remotePosition.value = remotePosition
120+
Storage.shared.nightscoutPosition.value = nightscoutPosition
121+
122+
// Dismiss the modal
123+
isPresented = false
124+
125+
// Call the completion handler after a small delay to ensure modal is dismissed
126+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
127+
onApply()
128+
}
129+
}
130+
}
131+
132+
struct TabPositionRow: View {
133+
let title: String
134+
let icon: String
135+
@Binding var position: TabPosition
136+
let otherPositions: [TabPosition]
137+
138+
var availablePositions: [TabPosition] {
139+
TabPosition.allCases.filter { tabPosition in
140+
// Always allow current position and disabled/more
141+
if tabPosition == position || tabPosition == .more || tabPosition == .disabled {
142+
return true
143+
}
144+
// Otherwise, only allow if not taken by another position
145+
return !otherPositions.contains(tabPosition)
146+
}
147+
}
148+
149+
var body: some View {
150+
HStack {
151+
Image(systemName: icon)
152+
.frame(width: 30)
153+
.foregroundColor(.accentColor)
154+
155+
Text(title)
156+
157+
Spacer()
158+
159+
Picker(title, selection: $position) {
160+
ForEach(availablePositions, id: \.self) { pos in
161+
Text(pos.displayName).tag(pos)
162+
}
163+
}
164+
.pickerStyle(.menu)
165+
.labelsHidden()
166+
}
167+
}
168+
}

LoopFollow/Storage/Storage+Migrate.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@
55
import Foundation
66

77
extension Storage {
8+
func migrateStep2() {
9+
// Migrate from old system to new position-based system
10+
if remoteType.value != .none {
11+
remotePosition.value = .position2
12+
alarmsPosition.value = .more
13+
} else {
14+
alarmsPosition.value = .position2
15+
remotePosition.value = .more
16+
}
17+
nightscoutPosition.value = .position4
18+
}
19+
820
func migrateStep1() {
921
Storage.shared.url.value = ObservableUserDefaults.shared.old_url.value
1022
Storage.shared.device.value = ObservableUserDefaults.shared.old_device.value

0 commit comments

Comments
 (0)