Przeglądaj źródła

Improved tab configuration

Jonas Björkert 1 rok temu
rodzic
commit
d9269da7b0

+ 4 - 4
LoopFollow.xcodeproj/project.pbxproj

@@ -43,7 +43,6 @@
 		DD1A97162D4294B3000DDC11 /* AdvancedSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A97152D4294B2000DDC11 /* AdvancedSettingsViewModel.swift */; };
 		DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52B82E1EB5DC00432050 /* TabPosition.swift */; };
 		DD1D52BB2E1EB60B00432050 /* MoreMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */; };
-		DD1D52BD2E1EB62E00432050 /* TabCustomizationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BC2E1EB62E00432050 /* TabCustomizationSettingsView.swift */; };
 		DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */; };
 		DD2C2E512D3B8B0C006413A5 /* NightscoutSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */; };
 		DD2C2E542D3C37DC006413A5 /* DexcomSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */; };
@@ -124,6 +123,7 @@
 		DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */; };
 		DD7F4C252DD7B20700D449E9 /* AlarmType+timeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */; };
 		DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; };
+		DD8060DB2E2ACE5900626B91 /* TabCustomizationModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8060DA2E2ACE5900626B91 /* TabCustomizationModal.swift */; };
 		DD8316182DE3633D004467AA /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316172DE3633D004467AA /* GeneralSettingsView.swift */; };
 		DD8316442DE47CA9004467AA /* BGPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316432DE47CA9004467AA /* BGPicker.swift */; };
 		DD8316462DE49B09004467AA /* GraphSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316452DE49B09004467AA /* GraphSettingsView.swift */; };
@@ -420,7 +420,6 @@
 		DD1A97152D4294B2000DDC11 /* AdvancedSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsViewModel.swift; sourceTree = "<group>"; };
 		DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = "<group>"; };
 		DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuViewController.swift; sourceTree = "<group>"; };
-		DD1D52BC2E1EB62E00432050 /* TabCustomizationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationSettingsView.swift; sourceTree = "<group>"; };
 		DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsView.swift; sourceTree = "<group>"; };
 		DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsViewModel.swift; sourceTree = "<group>"; };
 		DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSettingsViewModel.swift; sourceTree = "<group>"; };
@@ -500,6 +499,7 @@
 		DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+SortDirection.swift"; sourceTree = "<group>"; };
 		DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+timeUnit.swift"; sourceTree = "<group>"; };
 		DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = "<group>"; };
+		DD8060DA2E2ACE5900626B91 /* TabCustomizationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationModal.swift; sourceTree = "<group>"; };
 		DD8316172DE3633D004467AA /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = "<group>"; };
 		DD8316432DE47CA9004467AA /* BGPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGPicker.swift; sourceTree = "<group>"; };
 		DD8316452DE49B09004467AA /* GraphSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsView.swift; sourceTree = "<group>"; };
@@ -869,7 +869,7 @@
 		DD1A97122D429495000DDC11 /* Settings */ = {
 			isa = PBXGroup;
 			children = (
-				DD1D52BC2E1EB62E00432050 /* TabCustomizationSettingsView.swift */,
+				DD8060DA2E2ACE5900626B91 /* TabCustomizationModal.swift */,
 				DD83164F2DE4E635004467AA /* SettingsMenuView.swift */,
 				DD2C2E552D3C3913006413A5 /* DexcomSettingsView.swift */,
 				DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */,
@@ -1834,6 +1834,7 @@
 				DD0B9D582DE1F3B20090C337 /* AlarmType+canAcknowledge.swift in Sources */,
 				DD2C2E562D3C3917006413A5 /* DexcomSettingsView.swift in Sources */,
 				DD7F4BC52DD3CE0700D449E9 /* AlarmBGLimitSection.swift in Sources */,
+				DD8060DB2E2ACE5900626B91 /* TabCustomizationModal.swift in Sources */,
 				DD7F4B9F2DD1F92700D449E9 /* AlarmActiveSection.swift in Sources */,
 				DD4AFB672DB68C5500BB593F /* UUID+Identifiable.swift in Sources */,
 				DD9ED0CA2D355257000D2A63 /* LogView.swift in Sources */,
@@ -2003,7 +2004,6 @@
 				FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */,
 				DD4AFB6B2DB6BF2A00BB593F /* Binding+Optional.swift in Sources */,
 				DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */,
-				DD1D52BD2E1EB62E00432050 /* TabCustomizationSettingsView.swift in Sources */,
 				DD5DA27C2DC930D6003D44FC /* GlucoseValue.swift in Sources */,
 				DD9ACA062D32AF7900415D8A /* TreatmentsTask.swift in Sources */,
 				DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */,

+ 43 - 4
LoopFollow/Settings/SettingsMenuView.swift

@@ -15,6 +15,7 @@ struct SettingsMenuView: View {
 
     @State private var latestVersion: String?
     @State private var versionTint: Color = .secondary
+    @State private var showingTabCustomization = false
 
     // MARK: – Body
 
@@ -44,10 +45,10 @@ struct SettingsMenuView: View {
                         settingsPath.value.append(Sheet.graph)
                     }
 
-                    NavigationRow(title: "Tab Customization",
+                    NavigationRow(title: "Tab Settings",
                                   icon: "rectangle.3.group")
                     {
-                        settingsPath.value.append(Sheet.tabCustomization)
+                        showingTabCustomization = true
                     }
 
                     if !nightscoutURL.value.isEmpty {
@@ -128,6 +129,15 @@ struct SettingsMenuView: View {
             }
             .navigationTitle("Settings")
             .navigationDestination(for: Sheet.self) { $0.destination }
+            .sheet(isPresented: $showingTabCustomization) {
+                TabCustomizationModal(
+                    isPresented: $showingTabCustomization,
+                    onApply: {
+                        // Dismiss any presented view controller and go to home tab
+                        handleTabReorganization()
+                    }
+                )
+            }
         }
         .task { await refreshVersionInfo() }
     }
@@ -218,6 +228,37 @@ struct SettingsMenuView: View {
                                            applicationActivities: nil)
         UIApplication.shared.topMost?.present(avc, animated: true)
     }
+
+    private func handleTabReorganization() {
+        // Find the root tab bar controller
+        guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+              let window = windowScene.windows.first,
+              let rootVC = window.rootViewController else { return }
+
+        // Navigate through the hierarchy to find the tab bar controller
+        var tabBarController: UITabBarController?
+
+        if let tbc = rootVC as? UITabBarController {
+            tabBarController = tbc
+        } else if let nav = rootVC as? UINavigationController,
+                  let tbc = nav.viewControllers.first as? UITabBarController
+        {
+            tabBarController = tbc
+        }
+
+        guard let tabBar = tabBarController else { return }
+
+        // Dismiss any modals first
+        if let presented = tabBar.presentedViewController {
+            presented.dismiss(animated: false) {
+                // After dismissal, switch to home tab
+                tabBar.selectedIndex = 0
+            }
+        } else {
+            // No modal to dismiss, just switch to home
+            tabBar.selectedIndex = 0
+        }
+    }
 }
 
 // MARK: – Sheet routing
@@ -232,7 +273,6 @@ private enum Sheet: Hashable, Identifiable {
     case calendar, contact
     case advanced
     case viewLog
-    case tabCustomization
 
     var id: Self { self }
 
@@ -252,7 +292,6 @@ private enum Sheet: Hashable, Identifiable {
         case .contact: ContactSettingsView(viewModel: .init())
         case .advanced: AdvancedSettingsView(viewModel: .init())
         case .viewLog: LogView(viewModel: .init())
-        case .tabCustomization: TabCustomizationSettingsView()
         }
     }
 }

+ 168 - 0
LoopFollow/Settings/TabCustomizationModal.swift

@@ -0,0 +1,168 @@
+// LoopFollow
+// TabCustomizationModal.swift
+// Created by Jonas Björkert.
+
+import SwiftUI
+
+struct TabCustomizationModal: View {
+    @Binding var isPresented: Bool
+    let onApply: () -> Void
+
+    // Local state for editing
+    @State private var alarmsPosition: TabPosition
+    @State private var remotePosition: TabPosition
+    @State private var nightscoutPosition: TabPosition
+    @State private var hasChanges = false
+
+    // Store original values to detect changes
+    private let originalAlarmsPosition: TabPosition
+    private let originalRemotePosition: TabPosition
+    private let originalNightscoutPosition: TabPosition
+
+    init(isPresented: Binding<Bool>, onApply: @escaping () -> Void) {
+        _isPresented = isPresented
+        self.onApply = onApply
+
+        // Initialize with current values
+        let currentAlarms = Storage.shared.alarmsPosition.value
+        let currentRemote = Storage.shared.remotePosition.value
+        let currentNightscout = Storage.shared.nightscoutPosition.value
+
+        _alarmsPosition = State(initialValue: currentAlarms)
+        _remotePosition = State(initialValue: currentRemote)
+        _nightscoutPosition = State(initialValue: currentNightscout)
+
+        originalAlarmsPosition = currentAlarms
+        originalRemotePosition = currentRemote
+        originalNightscoutPosition = currentNightscout
+    }
+
+    var body: some View {
+        NavigationView {
+            Form {
+                Section("Tab Positions") {
+                    TabPositionRow(
+                        title: "Alarms",
+                        icon: "alarm",
+                        position: $alarmsPosition,
+                        otherPositions: [remotePosition, nightscoutPosition]
+                    )
+                    .onChange(of: alarmsPosition) { _ in checkForChanges() }
+
+                    TabPositionRow(
+                        title: "Remote",
+                        icon: "antenna.radiowaves.left.and.right",
+                        position: $remotePosition,
+                        otherPositions: [alarmsPosition, nightscoutPosition]
+                    )
+                    .onChange(of: remotePosition) { _ in checkForChanges() }
+
+                    TabPositionRow(
+                        title: "Nightscout",
+                        icon: "safari",
+                        position: $nightscoutPosition,
+                        otherPositions: [alarmsPosition, remotePosition]
+                    )
+                    .onChange(of: nightscoutPosition) { _ in checkForChanges() }
+                }
+
+                Section {
+                    Text("• Tab 2 and Tab 4 can each hold one item")
+                        .font(.caption)
+                        .foregroundColor(.secondary)
+                    Text("• Items in 'More Menu' appear under the last tab")
+                        .font(.caption)
+                        .foregroundColor(.secondary)
+                    Text("• Hidden items are not accessible")
+                        .font(.caption)
+                        .foregroundColor(.secondary)
+                }
+
+                if hasChanges {
+                    Section {
+                        Text("Changes will be applied when you tap 'Apply'")
+                            .font(.caption)
+                            .foregroundColor(.orange)
+                    }
+                }
+            }
+            .navigationTitle("Tab Settings")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .navigationBarLeading) {
+                    Button("Cancel") {
+                        isPresented = false
+                    }
+                }
+
+                ToolbarItem(placement: .navigationBarTrailing) {
+                    Button("Apply") {
+                        applyChanges()
+                    }
+                    .fontWeight(.semibold)
+                    .disabled(!hasChanges)
+                }
+            }
+        }
+        .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil)
+    }
+
+    private func checkForChanges() {
+        hasChanges = alarmsPosition != originalAlarmsPosition ||
+            remotePosition != originalRemotePosition ||
+            nightscoutPosition != originalNightscoutPosition
+    }
+
+    private func applyChanges() {
+        // Save the new positions
+        Storage.shared.alarmsPosition.value = alarmsPosition
+        Storage.shared.remotePosition.value = remotePosition
+        Storage.shared.nightscoutPosition.value = nightscoutPosition
+
+        // Dismiss the modal
+        isPresented = false
+
+        // Call the completion handler after a small delay to ensure modal is dismissed
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+            onApply()
+        }
+    }
+}
+
+struct TabPositionRow: View {
+    let title: String
+    let icon: String
+    @Binding var position: TabPosition
+    let otherPositions: [TabPosition]
+
+    var availablePositions: [TabPosition] {
+        TabPosition.allCases.filter { tabPosition in
+            // Always allow current position and disabled/more
+            if tabPosition == position || tabPosition == .more || tabPosition == .disabled {
+                return true
+            }
+            // Otherwise, only allow if not taken by another position
+            return !otherPositions.contains(tabPosition)
+        }
+    }
+
+    var body: some View {
+        HStack {
+            Image(systemName: icon)
+                .frame(width: 30)
+                .foregroundColor(.accentColor)
+
+            Text(title)
+
+            Spacer()
+
+            Picker(title, selection: $position) {
+                ForEach(availablePositions, id: \.self) { pos in
+                    Text(pos.displayName).tag(pos)
+                }
+            }
+            .pickerStyle(.menu)
+            .labelsHidden()
+        }
+    }
+}

+ 0 - 97
LoopFollow/Settings/TabCustomizationSettingsView.swift

@@ -1,97 +0,0 @@
-// LoopFollow
-// TabCustomizationSettingsView.swift
-// Created by Jonas Björkert.
-
-import SwiftUI
-
-struct TabCustomizationSettingsView: View {
-    // MARK: - Local State
-
-    @State private var alarmsPosition: TabPosition
-    @State private var remotePosition: TabPosition
-    @State private var nightscoutPosition: TabPosition
-
-    init() {
-        _alarmsPosition = State(initialValue: Storage.shared.alarmsPosition.value)
-        _remotePosition = State(initialValue: Storage.shared.remotePosition.value)
-        _nightscoutPosition = State(initialValue: Storage.shared.nightscoutPosition.value)
-    }
-
-    var body: some View {
-        Form {
-            Section("Tab Positions") {
-                TabPositionRow(
-                    title: "Alarms",
-                    icon: "alarm",
-                    position: $alarmsPosition,
-                    otherPositions: [remotePosition, nightscoutPosition]
-                )
-
-                TabPositionRow(
-                    title: "Remote",
-                    icon: "antenna.radiowaves.left.and.right",
-                    position: $remotePosition,
-                    otherPositions: [alarmsPosition, nightscoutPosition]
-                )
-
-                TabPositionRow(
-                    title: "Nightscout",
-                    icon: "safari",
-                    position: $nightscoutPosition,
-                    otherPositions: [alarmsPosition, remotePosition]
-                )
-            }
-
-            Section {
-                Text("• Tab 2 and Tab 4 can each hold one item")
-                Text("• Items in 'More Menu' appear under the last tab")
-                Text("• Hidden items are not accessible")
-            }
-        }
-        .navigationTitle("Tab Settings")
-        .navigationBarTitleDisplayMode(.inline)
-        .onDisappear {
-            Storage.shared.alarmsPosition.value = alarmsPosition
-            Storage.shared.remotePosition.value = remotePosition
-            Storage.shared.nightscoutPosition.value = nightscoutPosition
-        }
-    }
-}
-
-struct TabPositionRow: View {
-    let title: String
-    let icon: String
-    @Binding var position: TabPosition
-    let otherPositions: [TabPosition]
-
-    var availablePositions: [TabPosition] {
-        TabPosition.allCases.filter { tabPosition in
-            // Always allow current position and disabled/more
-            if tabPosition == position || tabPosition == .more || tabPosition == .disabled {
-                return true
-            }
-            // Otherwise, only allow if not taken by another position
-            return !otherPositions.contains(tabPosition)
-        }
-    }
-
-    var body: some View {
-        HStack {
-            Image(systemName: icon)
-                .frame(width: 30)
-                .foregroundColor(.accentColor)
-
-            Text(title)
-
-            Spacer()
-
-            Picker(title, selection: $position) {
-                ForEach(availablePositions, id: \.self) { pos in
-                    Text(pos.displayName).tag(pos)
-                }
-            }
-            .pickerStyle(.menu)
-            .labelsHidden()
-        }
-    }
-}

+ 62 - 14
LoopFollow/ViewControllers/MainViewController.swift

@@ -320,30 +320,78 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     private func setupTabBar() {
         guard let tabBarController = tabBarController else { return }
 
-        // Check if we're currently in the More tab and it's about to disappear
-        let wasInMoreTab = tabBarController.selectedIndex == 4 &&
+        // Store current selection before making changes
+        let currentSelectedIndex = tabBarController.selectedIndex
+
+        // Check if we need to handle More tab disappearing
+        let wasInMoreTab = currentSelectedIndex == 4 &&
             tabBarController.viewControllers?.last is MoreMenuViewController
         let willHaveMoreTab = hasItemsInMore()
 
-        // If we're in More tab and it's going away, handle the transition
+        // If currently in More tab and it's going away, we need to handle this carefully
         if wasInMoreTab && !willHaveMoreTab {
-            // First dismiss any modal that might be presented
-            if let moreVC = tabBarController.viewControllers?.last as? MoreMenuViewController,
-               let presented = moreVC.presentedViewController
-            {
+            // First, dismiss any modals that might be open
+            if let presented = tabBarController.presentedViewController {
                 presented.dismiss(animated: false) { [weak self] in
-                    // After dismissal, switch to home and setup tabs
-                    tabBarController.selectedIndex = 0
-                    self?.completeTabBarSetup(tabBarController: tabBarController, willHaveMoreTab: willHaveMoreTab)
+                    // After dismissal, rebuild tabs with home selected
+                    self?.rebuildTabs(tabBarController: tabBarController,
+                                      willHaveMoreTab: willHaveMoreTab,
+                                      selectedIndex: 0)
                 }
                 return
-            } else {
-                // No modal presented, just switch to home
-                tabBarController.selectedIndex = 0
             }
         }
 
-        completeTabBarSetup(tabBarController: tabBarController, willHaveMoreTab: willHaveMoreTab)
+        // For all other cases, rebuild tabs normally
+        rebuildTabs(tabBarController: tabBarController,
+                    willHaveMoreTab: willHaveMoreTab,
+                    selectedIndex: wasInMoreTab && !willHaveMoreTab ? 0 : currentSelectedIndex)
+    }
+
+    private func rebuildTabs(tabBarController: UITabBarController,
+                             willHaveMoreTab: Bool,
+                             selectedIndex: Int)
+    {
+        let storyboard = UIStoryboard(name: "Main", bundle: nil)
+        var viewControllers: [UIViewController] = []
+
+        // Tab 0 - Home (always)
+        viewControllers.append(self)
+
+        // Tab 1 - Dynamic based on what's assigned to position2
+        if let vc = createViewController(for: .position2, storyboard: storyboard) {
+            viewControllers.append(vc)
+        }
+
+        // Tab 2 - Snoozer (always)
+        let snoozerVC = storyboard.instantiateViewController(withIdentifier: "SnoozerViewController")
+        snoozerVC.tabBarItem = UITabBarItem(title: "Snoozer", image: UIImage(systemName: "zzz"), tag: 2)
+        viewControllers.append(snoozerVC)
+
+        // Tab 3 - Dynamic based on what's assigned to position4
+        if let vc = createViewController(for: .position4, storyboard: storyboard) {
+            viewControllers.append(vc)
+        }
+
+        // Tab 4 - Settings or More
+        if willHaveMoreTab {
+            let moreVC = MoreMenuViewController()
+            moreVC.tabBarItem = UITabBarItem(title: "More", image: UIImage(systemName: "ellipsis"), tag: 4)
+            viewControllers.append(moreVC)
+        } else {
+            let settingsVC = SettingsViewController()
+            settingsVC.tabBarItem = UITabBarItem(title: "Settings", image: UIImage(systemName: "gear"), tag: 4)
+            viewControllers.append(settingsVC)
+        }
+
+        // Update view controllers without animation to prevent glitches
+        tabBarController.setViewControllers(viewControllers, animated: false)
+
+        // Restore selection if valid, otherwise default to home
+        let safeIndex = min(selectedIndex, viewControllers.count - 1)
+        tabBarController.selectedIndex = max(0, safeIndex)
+
+        updateNightscoutTabState()
     }
 
     private func completeTabBarSetup(tabBarController: UITabBarController, willHaveMoreTab: Bool) {