فهرست منبع

Reorganize settings navigation and tab customization (#553)

Daniel Mini Johansson 4 ماه پیش
والد
کامیت
99cf93c8f9

+ 4 - 4
LoopFollow/Alarm/AlarmsContainerView.swift

@@ -4,16 +4,16 @@
 import SwiftUI
 
 struct AlarmsContainerView: View {
-    var onDismiss: (() -> Void)?
+    var onBack: (() -> Void)?
 
     var body: some View {
         NavigationStack {
             AlarmListView()
                 .toolbar {
-                    if let onDismiss {
+                    if let onBack {
                         ToolbarItem(placement: .navigationBarLeading) {
-                            Button(action: onDismiss) {
-                                Image(systemName: "checkmark")
+                            Button(action: onBack) {
+                                Image(systemName: "chevron.left")
                             }
                         }
                     }

+ 6 - 1
LoopFollow/Helpers/TabPosition.swift

@@ -89,8 +89,13 @@ enum TabItem: String, CaseIterable, Codable, Identifiable {
         }
     }
 
+    /// Canonical feature order used by menus and customization screens.
+    static var featureOrder: [TabItem] {
+        [.home, .alarms, .nightscout, .remote, .snoozer, .stats, .treatments]
+    }
+
     /// Items that can be moved between tab bar and menu (all except settings which doesn't exist as a tab)
     static var movableItems: [TabItem] {
-        [.home, .alarms, .remote, .nightscout, .snoozer, .treatments, .stats]
+        featureOrder
     }
 }

+ 11 - 1
LoopFollow/Log/LogView.swift

@@ -4,6 +4,7 @@
 import SwiftUI
 
 struct LogView: View {
+    var onBack: (() -> Void)?
     @ObservedObject var viewModel = LogViewModel()
 
     var body: some View {
@@ -35,8 +36,17 @@ struct LogView: View {
             .onAppear {
                 viewModel.loadLogEntries()
             }
+            .navigationBarTitle("Today's Logs", displayMode: .inline)
+            .toolbar {
+                if let onBack {
+                    ToolbarItem(placement: .navigationBarLeading) {
+                        Button(action: onBack) {
+                            Image(systemName: "chevron.left")
+                        }
+                    }
+                }
+            }
         }
         .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
-        .navigationBarTitle("Today's Logs", displayMode: .inline)
     }
 }

+ 2 - 3
LoopFollow/Settings/HomeContentView.swift

@@ -55,13 +55,12 @@ struct HomeModalView: View {
                 .navigationTitle("Home")
                 .navigationBarTitleDisplayMode(.inline)
                 .toolbar {
-                    ToolbarItem(placement: .navigationBarTrailing) {
+                    ToolbarItem(placement: .cancellationAction) {
                         Button {
                             dismiss()
                         } label: {
-                            Image(systemName: "checkmark")
+                            Image(systemName: "xmark")
                         }
-                        .foregroundColor(.blue)
                     }
                 }
         }

+ 1 - 1
LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift

@@ -98,7 +98,7 @@ struct ImportExportSettingsView: View {
                 }
                 .navigationTitle("Export \(viewModel.exportType.rawValue)")
                 .navigationBarTitleDisplayMode(.inline)
-                .navigationBarItems(trailing: Button("Done") {
+                .navigationBarItems(trailing: Button("Close") {
                     viewModel.isShowingQRCodeDisplay = false
                 })
             }

+ 27 - 40
LoopFollow/Settings/SettingsMenuView.swift

@@ -12,8 +12,7 @@ struct SettingsMenuView: View {
 
     // MARK: – Local state
 
-    @State private var showingTabCustomization = false
-    var onDismiss: (() -> Void)?
+    var onBack: (() -> Void)?
 
     // MARK: – Observed objects
 
@@ -24,33 +23,40 @@ struct SettingsMenuView: View {
     var body: some View {
         NavigationStack(path: $settingsPath.value) {
             List {
-                // ───────── Data settings ─────────
                 dataSection
 
-                // ───────── App settings ─────────
-                Section("App Settings") {
-                    NavigationRow(title: "Background Refresh",
-                                  icon: "arrow.clockwise")
-                    {
-                        settingsPath.value.append(Sheet.backgroundRefresh)
-                    }
-
+                Section("Display Settings") {
                     NavigationRow(title: "General",
                                   icon: "gearshape")
                     {
                         settingsPath.value.append(Sheet.general)
                     }
-
                     NavigationRow(title: "Graph",
                                   icon: "chart.xyaxis.line")
                     {
                         settingsPath.value.append(Sheet.graph)
                     }
 
-                    NavigationRow(title: "Tab Settings",
+                    if !nightscoutURL.value.isEmpty {
+                        NavigationRow(title: "Information Display",
+                                      icon: "info.circle")
+                        {
+                            settingsPath.value.append(Sheet.infoDisplay)
+                        }
+                    }
+
+                    NavigationRow(title: "Tabs",
                                   icon: "rectangle.3.group")
                     {
-                        showingTabCustomization = true
+                        settingsPath.value.append(Sheet.tabSettings)
+                    }
+                }
+
+                Section("App Settings") {
+                    NavigationRow(title: "Background Refresh",
+                                  icon: "arrow.clockwise")
+                    {
+                        settingsPath.value.append(Sheet.backgroundRefresh)
                     }
 
                     NavigationRow(title: "Import/Export",
@@ -60,12 +66,6 @@ struct SettingsMenuView: View {
                     }
 
                     if !nightscoutURL.value.isEmpty {
-                        NavigationRow(title: "Information Display",
-                                      icon: "info.circle")
-                        {
-                            settingsPath.value.append(Sheet.infoDisplay)
-                        }
-
                         NavigationRow(title: "Remote",
                                       icon: "antenna.radiowaves.left.and.right")
                         {
@@ -74,7 +74,6 @@ struct SettingsMenuView: View {
                     }
                 }
 
-                // ───────── Alarms ─────────
                 Section("Alarms") {
                     NavigationRow(title: "Alarms",
                                   icon: "bell.badge")
@@ -83,7 +82,6 @@ struct SettingsMenuView: View {
                     }
                 }
 
-                // ───────── Integrations ─────────
                 Section("Integrations") {
                     NavigationRow(title: "Calendar",
                                   icon: "calendar")
@@ -98,7 +96,6 @@ struct SettingsMenuView: View {
                     }
                 }
 
-                // ───────── Advanced ─────────
                 Section("Advanced Settings") {
                     NavigationRow(title: "Advanced",
                                   icon: "exclamationmark.shield")
@@ -108,24 +105,17 @@ struct SettingsMenuView: View {
                 }
             }
             .navigationTitle("Settings")
+            .navigationBarTitleDisplayMode(.large)
             .navigationDestination(for: Sheet.self) { $0.destination }
             .toolbar {
-                if let onDismiss {
-                    ToolbarItem(placement: .navigationBarTrailing) {
-                        Button(action: onDismiss) {
-                            Image(systemName: "checkmark")
+                if let onBack {
+                    ToolbarItem(placement: .navigationBarLeading) {
+                        Button(action: onBack) {
+                            Image(systemName: "chevron.left")
                         }
                     }
                 }
             }
-            .sheet(isPresented: $showingTabCustomization) {
-                TabCustomizationModal(
-                    isPresented: $showingTabCustomization,
-                    onApply: {
-                        // No-op - changes are applied silently via observers
-                    }
-                )
-            }
         }
     }
 
@@ -157,11 +147,6 @@ struct SettingsMenuView: View {
             }
         }
     }
-
-    private func handleTabReorganization() {
-        // Rebuild the tab bar with the new configuration
-        MainViewController.rebuildTabsIfNeeded()
-    }
 }
 
 // MARK: – Sheet routing
@@ -170,6 +155,7 @@ private enum Sheet: Hashable, Identifiable {
     case nightscout, dexcom
     case backgroundRefresh
     case general, graph
+    case tabSettings
     case infoDisplay
     case alarmSettings
     case remote
@@ -188,6 +174,7 @@ private enum Sheet: Hashable, Identifiable {
         case .backgroundRefresh: BackgroundRefreshSettingsView(viewModel: .init())
         case .general: GeneralSettingsView()
         case .graph: GraphSettingsView()
+        case .tabSettings: TabCustomizationModal()
         case .infoDisplay: InfoDisplaySettingsView(viewModel: .init())
         case .alarmSettings: AlarmSettingsView()
         case .remote: RemoteSettingsView(viewModel: .init())

+ 84 - 128
LoopFollow/Settings/TabCustomizationModal.swift

@@ -31,17 +31,11 @@ enum TabCustomizationItem: Identifiable, Equatable, Hashable {
 }
 
 struct TabCustomizationModal: View {
-    @Binding var isPresented: Bool
-    let onApply: () -> Void
-
     // All items including Settings - top 4 go to tab bar, rest to menu
     @State private var allItems: [TabCustomizationItem]
     private let originalItems: [TabCustomizationItem]
 
-    init(isPresented: Binding<Bool>, onApply: @escaping () -> Void) {
-        _isPresented = isPresented
-        self.onApply = onApply
-
+    init() {
         let sortedTabItems = TabItem.movableItems.sorted { item1, item2 in
             let pos1 = Storage.shared.position(for: item1).normalized
             let pos2 = Storage.shared.position(for: item2).normalized
@@ -84,152 +78,114 @@ struct TabCustomizationModal: View {
     }
 
     var body: some View {
-        NavigationStack {
-            List {
-                // Instructions
-                Section {
-                    VStack(alignment: .leading, spacing: 8) {
-                        Text("Drag to reorder")
-                            .font(.subheadline)
-                            .fontWeight(.medium)
-                        Text("The top 4 items appear in the tab bar. Items 5+ appear in the Menu.")
-                            .font(.caption)
-                            .foregroundColor(.secondary)
+        List {
+            // Instructions
+            Section {
+                VStack(alignment: .leading, spacing: 8) {
+                    Text("Drag to reorder")
+                        .font(.subheadline)
+                        .fontWeight(.medium)
+                    Text("The top 4 items appear in the tab bar. The Menu can always open every feature.")
+                        .font(.caption)
+                        .foregroundColor(.secondary)
+                }
+                .padding(.vertical, 4)
+            }
+
+            // All items - Settings appears at position 5 as a divider
+            Section {
+                // Build display list: first 4 TabItems, then Settings, then remaining TabItems
+                let tabItems = allItems.compactMap { item -> TabItem? in
+                    if case let .tabItem(tabItem) = item { return tabItem }
+                    return nil
+                }
+
+                // Display items in order: tab bar items, Settings, menu items
+                ForEach(Array(allItems.enumerated()), id: \.element) { _, item in
+                    switch item {
+                    case let .tabItem(tabItem):
+                        // Determine if this TabItem is in tab bar or menu
+                        let tabItemIndex = tabItems.firstIndex(of: tabItem) ?? 0
+                        let isInTabBar = tabItemIndex < 4
+
+                        TabItemRow(
+                            item : tabItem,
+                            position: isInTabBar ? tabItemIndex + 1: nil,
+                            isInMenu: !isInTabBar
+                        )
+                    case .settings:
+                        SettingsRow()
+                            .moveDisabled(true)
                     }
-                    .padding(.vertical, 4)
                 }
+                .onMove { source, destination in
+                    // Check if Settings (at index 4) is being moved - prevent it
+                    if source.contains(4) {
+                        return
+                    }
 
-                // All items - Settings appears at position 5 as a divider
-                Section {
-                    // Build display list: first 4 TabItems, then Settings, then remaining TabItems
-                    let tabItems = allItems.compactMap { item -> TabItem? in
+                    // Get all TabItems (excluding Settings)
+                    var tabItemsOnly: [TabItem] = allItems.compactMap { item -> TabItem? in
                         if case let .tabItem(tabItem) = item { return tabItem }
                         return nil
                     }
 
-                    // Display items in order: tab bar items, Settings, menu items
-                    ForEach(Array(allItems.enumerated()), id: \.element) { _, item in
-                        switch item {
-                        case let .tabItem(tabItem):
-                            // Determine if this TabItem is in tab bar or menu
-                            let tabItemIndex = tabItems.firstIndex(of: tabItem) ?? 0
-                            let isInTabBar = tabItemIndex < 4
-
-                            TabItemRow(
-                                item : tabItem,
-                                position: isInTabBar ? tabItemIndex + 1: nil,
-                                isInMenu: !isInTabBar
-                            )
-                        case .settings:
-                            SettingsRow()
-                                .moveDisabled(true)
-                        }
+                    // Adjust source indices: if any are after Settings (index 4), subtract 1
+                    var adjustedSource = source
+                    if source.contains(where: { $0 > 4 }) {
+                        adjustedSource = IndexSet(source.map { $0 > 4 ? $0 - 1 : $0 })
                     }
-                    .onMove { source, destination in
-                        // Check if Settings (at index 4) is being moved - prevent it
-                        if source.contains(4) {
-                            return
-                        }
 
-                        // Get all TabItems (excluding Settings)
-                        var tabItemsOnly: [TabItem] = allItems.compactMap { item -> TabItem? in
-                            if case let .tabItem(tabItem) = item { return tabItem }
-                            return nil
-                        }
-
-                        // Adjust source indices: if any are after Settings (index 4), subtract 1
-                        var adjustedSource = source
-                        if source.contains(where: { $0 > 4 }) {
-                            adjustedSource = IndexSet(source.map { $0 > 4 ? $0 - 1 : $0 })
-                        }
+                    // Adjust destination: if it's after Settings (position 5), subtract 1
+                    let adjustedDestination = destination > 4 ? destination - 1 : destination
 
-                        // Adjust destination: if it's after Settings (position 5), subtract 1
-                        let adjustedDestination = destination > 4 ? destination - 1 : destination
+                    // Move TabItems
+                    tabItemsOnly.move(fromOffsets: adjustedSource, toOffset: adjustedDestination)
 
-                        // Move TabItems
-                        tabItemsOnly.move(fromOffsets: adjustedSource, toOffset: adjustedDestination)
-
-                        // Rebuild allItems with Settings at position 5
-                        var newItems: [TabCustomizationItem] = []
-                        for (index, tabItem) in tabItemsOnly.enumerated() {
-                            newItems.append(.tabItem(tabItem))
-                            // Insert Settings after the 4th TabItem (at position 5)
-                            if index == 3 {
-                                newItems.append(.settings)
-                            }
-                        }
-
-                        // If there are fewer than 4 TabItems, add Settings after the last one
-                        if tabItemsOnly.count < 4 {
+                    // Rebuild allItems with Settings at position 5
+                    var newItems: [TabCustomizationItem] = []
+                    for (index, tabItem) in tabItemsOnly.enumerated() {
+                        newItems.append(.tabItem(tabItem))
+                        // Insert Settings after the 4th TabItem (at position 5)
+                        if index == 3 {
                             newItems.append(.settings)
                         }
-
-                        allItems = newItems
-                    }
-                } header: {
-                    Text("Tab Order")
-                }
-            }
-            .listStyle(.insetGrouped)
-            .environment(\.editMode, .constant(.active))
-            .navigationTitle("Tab Settings")
-            .navigationBarTitleDisplayMode(.inline)
-            .toolbar {
-                ToolbarItem(placement: .navigationBarLeading) {
-                    Button {
-                        allItems = originalItems
-                        isPresented = false
-                    } label: {
-                        HStack(spacing: 4) {
-                            Image(systemName: "chevron.left")
-                            Text("Back")
-                        }
                     }
-                }
-                ToolbarItem(placement: .navigationBarTrailing) {
-                    Button {
-                        applyChangesSilently()
-                        onApply()
-                        isPresented = false
-                    } label: {
-                        Image(systemName: "checkmark")
+
+                    // If there are fewer than 4 TabItems, add Settings after the last one
+                    if tabItemsOnly.count < 4 {
+                        newItems.append(.settings)
                     }
-                    .disabled(allItems == originalItems)
-                    .foregroundColor(.blue)
+
+                    allItems = newItems
+                    persistTabOrder(tabItemsOnly)
                 }
+            } header: {
+                Text("Tab Order")
             }
         }
+        .listStyle(.insetGrouped)
+        .environment(\.editMode, .constant(.active))
+        .navigationTitle("Tabs")
+        .navigationBarTitleDisplayMode(.inline)
         .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
     }
 
     // MARK: - Actions
 
-    private func applyChangesSilently() {
-        // Count only TabItems (not Settings) to determine tab bar positions
-        // First 4 TabItems go to tab bar, rest go to menu
-        var tabItemCount = 0
-        for item in allItems {
-            switch item {
-            case let .tabItem(tabItem):
-                let position: TabPosition
-                if tabItemCount < 4 {
-                    switch tabItemCount {
-                    case 0: position = .position1
-                    case 1: position = .position2
-                    case 2: position = .position3
-                    case 3: position = .position4
-                    default: position = .menu
-                    }
-                } else {
-                    position = .menu
-                }
-                Storage.shared.setPosition(position, for: tabItem)
-                tabItemCount += 1
-            case .settings:
-                break
+    private func persistTabOrder(_ tabItems: [TabItem]) {
+        for (index, tabItem) in tabItems.enumerated() {
+            let position: TabPosition
+            switch index {
+            case 0: position = .position1
+            case 1: position = .position2
+            case 2: position = .position3
+            case 3: position = .position4
+            default: position = .menu
             }
+
+            Storage.shared.setPosition(position, for: tabItem)
         }
-        // Don't call onApply() - let the tab position observers handle the rebuild naturally
     }
 }
 

+ 1 - 1
LoopFollow/Stats/AggregatedStatsView.swift

@@ -105,7 +105,7 @@ struct AggregatedStatsView: View {
         }
         .navigationBarTitleDisplayMode(.inline)
         .toolbar {
-            ToolbarItem(placement: .navigationBarLeading) {
+            ToolbarItem(placement: .navigationBarTrailing) {
                 Button("Refresh") {
                     loadingError = false
                     isLoadingData = true

+ 14 - 0
LoopFollow/Storage/Storage+Migrate.swift

@@ -4,6 +4,20 @@
 import Foundation
 
 extension Storage {
+    func migrateStep5() {
+        let isLegacyDefaultTabBarOrder = homePosition.value.normalized == .position1
+            && alarmsPosition.value.normalized == .position2
+            && remotePosition.value.normalized == .position3
+            && nightscoutPosition.value.normalized == .position4
+            && snoozerPosition.value.normalized == .menu
+
+        guard isLegacyDefaultTabBarOrder else { return }
+
+        LogManager.shared.log(category: .general, message: "migrateStep5: Reordering default tabs to Home, Alarms, Nightscout, Remote")
+        nightscoutPosition.value = .position3
+        remotePosition.value = .position4
+    }
+
     func migrateStep3() {
         LogManager.shared.log(category: .general, message: "Running migrateStep3 - this should only happen once!")
         let legacyForceDarkMode = StorageValue<Bool>(key: "forceDarkMode", defaultValue: true)

+ 4 - 4
LoopFollow/Storage/Storage.swift

@@ -170,9 +170,9 @@ class Storage {
     // Tab positions - which position each item is in (positions 1-4 are customizable, 5 is always Menu)
     var homePosition = StorageValue<TabPosition>(key: "homePosition", defaultValue: .position1)
     var alarmsPosition = StorageValue<TabPosition>(key: "alarmsPosition", defaultValue: .position2)
-    var snoozerPosition = StorageValue<TabPosition>(key: "snoozerPosition", defaultValue: .position3)
-    var nightscoutPosition = StorageValue<TabPosition>(key: "nightscoutPosition", defaultValue: .position4)
-    var remotePosition = StorageValue<TabPosition>(key: "remotePosition", defaultValue: .menu)
+    var snoozerPosition = StorageValue<TabPosition>(key: "snoozerPosition", defaultValue: .menu)
+    var nightscoutPosition = StorageValue<TabPosition>(key: "nightscoutPosition", defaultValue: .position3)
+    var remotePosition = StorageValue<TabPosition>(key: "remotePosition", defaultValue: .position4)
     var statisticsPosition = StorageValue<TabPosition>(key: "statisticsPosition", defaultValue: .menu)
     var treatmentsPosition = StorageValue<TabPosition>(key: "treatmentsPosition", defaultValue: .menu)
 
@@ -232,7 +232,7 @@ class Storage {
 
     /// Get all items in the Menu (position 5)
     func itemsInMenu() -> [TabItem] {
-        TabItem.allCases.filter { position(for: $0).normalized == .menu }
+        TabItem.featureOrder.filter { position(for: $0).normalized == .menu }
     }
 
     /// Get items ordered by their position in the tab bar (positions 1-4)

+ 11 - 0
LoopFollow/Treatments/TreatmentsView.swift

@@ -17,6 +17,8 @@ private func formatBG(_ mgdlValue: Int, units: String = Storage.shared.units.val
 }
 
 struct TreatmentsView: View {
+    var onBack: (() -> Void)?
+
     @StateObject private var viewModel = TreatmentsViewModel()
     @State private var selectedFilter: TreatmentFilter = .all
     @ObservedObject private var device = Storage.shared.device
@@ -175,6 +177,15 @@ struct TreatmentsView: View {
                 }
                 .navigationTitle("")
                 .navigationBarTitleDisplayMode(.inline)
+                .toolbar {
+                    if let onBack {
+                        ToolbarItem(placement: .navigationBarLeading) {
+                            Button(action: onBack) {
+                                Image(systemName: "chevron.left")
+                            }
+                        }
+                    }
+                }
                 .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
                 .refreshable {
                     viewModel.refreshTreatments()

+ 48 - 27
LoopFollow/ViewControllers/MainViewController.swift

@@ -168,6 +168,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
             Storage.shared.migrationStep.value = 4
         }
 
+        if Storage.shared.migrationStep.value < 5 {
+            Storage.shared.migrateStep5()
+            Storage.shared.migrationStep.value = 5
+        }
+
         // Synchronize info types to ensure arrays are the correct size
         synchronizeInfoTypes()
 
@@ -598,9 +603,16 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
             }
         }
 
-        let menuVC = MoreMenuViewController()
-        menuVC.tabBarItem = UITabBarItem(title: "Menu", image: UIImage(systemName: "line.3.horizontal"), tag: 4)
-        viewControllers.append(menuVC)
+        // Preserve existing Menu nav controller to keep its push stack intact
+        let existingMenuNav = (tabBarController.viewControllers ?? []).first(where: {
+            $0.tabBarItem.title == "Menu"
+        })
+        if let menuNav = existingMenuNav {
+            menuNav.tabBarItem = UITabBarItem(title: "Menu", image: UIImage(systemName: "line.3.horizontal"), tag: 4)
+            viewControllers.append(menuNav)
+        } else {
+            viewControllers.append(Self.makeMenuViewController(tag: 4))
+        }
 
         if let presented = tabBarController.presentedViewController {
             presented.dismiss(animated: false) {
@@ -655,10 +667,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
             return treatmentsVC
 
         case .stats:
-            // You may need to provide a MainViewController or view model as needed
             let statsVC = UIHostingController(rootView: AggregatedStatsView(viewModel: AggregatedStatsViewModel(mainViewController: nil)))
-            statsVC.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag)
-            return statsVC
+            let navController = UINavigationController(rootViewController: statsVC)
+            navController.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag)
+            return navController
         }
     }
 
@@ -677,9 +689,16 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
             }
         }
 
-        let menuVC = MoreMenuViewController()
-        menuVC.tabBarItem = UITabBarItem(title: "Menu", image: UIImage(systemName: "line.3.horizontal"), tag: 4)
-        viewControllers.append(menuVC)
+        // Preserve existing Menu nav controller to keep its push stack intact
+        let existingMenuNav = (tabBarController.viewControllers ?? []).first(where: {
+            $0.tabBarItem.title == "Menu"
+        })
+        if let menuNav = existingMenuNav {
+            menuNav.tabBarItem = UITabBarItem(title: "Menu", image: UIImage(systemName: "line.3.horizontal"), tag: 4)
+            viewControllers.append(menuNav)
+        } else {
+            viewControllers.append(Self.makeMenuViewController(tag: 4))
+        }
 
         tabBarController.setViewControllers(viewControllers, animated: false)
 
@@ -738,11 +757,21 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
 
         case .stats:
             let statsVC = UIHostingController(rootView: AggregatedStatsView(viewModel: AggregatedStatsViewModel(mainViewController: self)))
-            statsVC.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag)
-            return statsVC
+            let navController = UINavigationController(rootViewController: statsVC)
+            navController.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag)
+            return navController
         }
     }
 
+    private static func makeMenuViewController(tag: Int) -> UIViewController {
+        let menuVC = MoreMenuViewController()
+        let navController = UINavigationController(rootViewController: menuVC)
+        navController.navigationBar.prefersLargeTitles = true
+        navController.tabBarItem = UITabBarItem(title: "Menu", image: UIImage(systemName: "line.3.horizontal"), tag: tag)
+        navController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+        return navController
+    }
+
     private func createComingSoonViewController(title: String, icon: String) -> UIViewController {
         let vc = UIViewController()
         vc.view.backgroundColor = .systemBackground
@@ -1406,14 +1435,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         hostingController.overrideUserInterfaceStyle = style
         navController.overrideUserInterfaceStyle = style
 
-        // Add a Done button
-        hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem(
-            image: UIImage(systemName: "checkmark"),
-            style: .plain,
-            target: self,
-            action: #selector(dismissModal)
-        )
-        hostingController.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
+        hostingController.navigationItem.rightBarButtonItem = makeCloseBarButtonItem()
 
         navController.modalPresentationStyle = .pageSheet
         present(navController, animated: true)
@@ -1429,14 +1451,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         hostingController.overrideUserInterfaceStyle = style
         navController.overrideUserInterfaceStyle = style
 
-        // Add a Done button
-        hostingController.navigationItem.rightBarButtonItem = UIBarButtonItem(
-            image: UIImage(systemName: "checkmark"),
-            style: .plain,
-            target: self,
-            action: #selector(dismissModal)
-        )
-        hostingController.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
+        hostingController.navigationItem.rightBarButtonItem = makeCloseBarButtonItem()
 
         navController.modalPresentationStyle = .pageSheet
         present(navController, animated: true)
@@ -1451,6 +1466,12 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         updateGraphVisibility()
     }
 
+    private func makeCloseBarButtonItem() -> UIBarButtonItem {
+        let button = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(dismissModal))
+        button.tintColor = .systemBlue
+        return button
+    }
+
     private func hideAllDataUI() {
         // Hide graphs
         BGChart.isHidden = true

+ 76 - 157
LoopFollow/ViewControllers/MoreMenuViewController.swift

@@ -9,6 +9,7 @@ class MoreMenuViewController: UIViewController {
     private var tableView: UITableView!
     private var cancellables = Set<AnyCancellable>()
     private var fallbackMainViewController: MainViewController?
+    var needsTabRebuild = false
 
     // Build Information state
     private var latestVersion: String?
@@ -50,6 +51,9 @@ class MoreMenuViewController: UIViewController {
         super.viewDidLoad()
 
         view.backgroundColor = .systemBackground
+        navigationItem.title = "Menu"
+        navigationItem.largeTitleDisplayMode = .always
+        navigationItem.backButtonDisplayMode = .minimal
 
         // Apply appearance mode
         overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
@@ -80,9 +84,16 @@ class MoreMenuViewController: UIViewController {
 
     override func viewWillAppear(_ animated: Bool) {
         super.viewWillAppear(animated)
+        navigationController?.setNavigationBarHidden(false, animated: animated)
+        navigationController?.navigationBar.prefersLargeTitles = true
         updateMenuItems()
         tableView.reloadData()
         Observable.shared.settingsPath.set(NavigationPath())
+
+        if needsTabRebuild {
+            needsTabRebuild = false
+            MainViewController.rebuildTabsIfNeeded()
+        }
     }
 
     override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
@@ -103,30 +114,12 @@ class MoreMenuViewController: UIViewController {
         tableView.delegate = self
         tableView.dataSource = self
         tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
-
-        // Large-title style header
-        let header = UIView()
-        let label = UILabel()
-        label.text = "Menu"
-        label.font = .systemFont(ofSize: 34, weight: .bold)
-        label.translatesAutoresizingMaskIntoConstraints = false
-        header.addSubview(label)
-        NSLayoutConstraint.activate([
-            label.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: 20),
-            label.topAnchor.constraint(equalTo: header.topAnchor, constant: 8),
-            label.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -8),
-        ])
-        header.frame.size = header.systemLayoutSizeFitting(
-            CGSize(width: view.bounds.width, height: UIView.layoutFittingCompressedSize.height),
-            withHorizontalFittingPriority: .required,
-            verticalFittingPriority: .fittingSizeLevel
-        )
-        tableView.tableHeaderView = header
+        tableView.contentInsetAdjustmentBehavior = .automatic
 
         view.addSubview(tableView)
 
         NSLayoutConstraint.activate([
-            tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            tableView.topAnchor.constraint(equalTo: view.topAnchor),
             tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
             tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
             tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
@@ -139,22 +132,23 @@ class MoreMenuViewController: UIViewController {
         let build = BuildDetails.default
         let ver = AppVersionManager().version()
 
-        menuSections = [
-            // Section 0: Settings
+        var sections: [MenuSection] = [
             MenuSection(title: nil, items: [
-                MenuItem(title: "Settings", icon: "gear") { [weak self] in
+                MenuItem(title: "Settings", icon: "gearshape") { [weak self] in
                     self?.openSettings()
                 },
             ]),
+        ]
 
-            // Section 1: All tab items (static)
-            MenuSection(title: "Features", items: TabItem.allCases.map { item in
+        sections.append(
+            MenuSection(title: "Features", items: TabItem.featureOrder.map { item in
                 MenuItem(title: item.displayName, icon: item.icon) { [weak self] in
                     self?.openItem(item)
                 }
-            }),
+            })
+        )
 
-            // Section 2: Logging
+        sections.append(contentsOf: [
             MenuSection(title: "Logging", items: [
                 MenuItem(title: "View Log", icon: "doc.text.magnifyingglass") { [weak self] in
                     self?.openViewLog()
@@ -205,7 +199,9 @@ class MoreMenuViewController: UIViewController {
 
                 return items
             }()),
-        ]
+        ])
+
+        menuSections = sections
     }
 
     // MARK: - Version fetching
@@ -237,11 +233,11 @@ class MoreMenuViewController: UIViewController {
             tabVC.selectedIndex = index
             return
         }
-        // Otherwise present as modal
-        presentItemAsModal(item)
+        // Otherwise push onto navigation stack
+        pushItem(item)
     }
 
-    private func presentItemAsModal(_ item: TabItem) {
+    private func pushItem(_ item: TabItem) {
         switch item {
         case .home:
             openHome()
@@ -261,114 +257,55 @@ class MoreMenuViewController: UIViewController {
     }
 
     private func openSettings() {
-        let settingsView = SettingsMenuView(onDismiss: { [weak self] in
-            self?.dismiss(animated: true) {
-                MainViewController.rebuildTabsIfNeeded()
-            }
+        needsTabRebuild = true
+        let settingsView = SettingsMenuView(onBack: { [weak self] in
+            self?.navigationController?.popViewController(animated: true)
         })
-        let settingsVC = UIHostingController(rootView: settingsView)
-
-        let style = Storage.shared.appearanceMode.value.userInterfaceStyle
-        settingsVC.overrideUserInterfaceStyle = style
-
-        settingsVC.modalPresentationStyle = .fullScreen
-        present(settingsVC, animated: true)
+        let settingsVC = NavBarHidingHostingController(rootView: settingsView)
+        settingsVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+        navigationController?.pushViewController(settingsVC, animated: true)
     }
 
     private func openAlarmsConfig() {
-        let alarmsView = AlarmsContainerView(onDismiss: { [weak self] in
-            self?.dismiss(animated: true)
+        let alarmsView = AlarmsContainerView(onBack: { [weak self] in
+            self?.navigationController?.popViewController(animated: true)
         })
-        let alarmsVC = UIHostingController(rootView: alarmsView)
-
-        let style = Storage.shared.appearanceMode.value.userInterfaceStyle
-        alarmsVC.overrideUserInterfaceStyle = style
-
-        alarmsVC.modalPresentationStyle = .fullScreen
-        present(alarmsVC, animated: true)
+        let alarmsVC = NavBarHidingHostingController(rootView: alarmsView)
+        alarmsVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+        navigationController?.pushViewController(alarmsVC, animated: true)
     }
 
     private func openRemote() {
         let storyboard = UIStoryboard(name: "Main", bundle: nil)
         let remoteVC = storyboard.instantiateViewController(withIdentifier: "RemoteViewController")
-        let navController = UINavigationController(rootViewController: remoteVC)
-
-        let style = Storage.shared.appearanceMode.value.userInterfaceStyle
-        remoteVC.overrideUserInterfaceStyle = style
-        navController.overrideUserInterfaceStyle = style
-
-        remoteVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
-            image: UIImage(systemName: "checkmark"),
-            style: .plain,
-            target: self,
-            action: #selector(dismissModal)
-        )
-        remoteVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
-
-        navController.modalPresentationStyle = .fullScreen
-        present(navController, animated: true)
+        remoteVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+        navigationController?.pushViewController(remoteVC, animated: true)
+        remoteVC.navigationItem.largeTitleDisplayMode = .never
     }
 
     private func openNightscout() {
         let storyboard = UIStoryboard(name: "Main", bundle: nil)
         let nightscoutVC = storyboard.instantiateViewController(withIdentifier: "NightscoutViewController")
-        let navController = UINavigationController(rootViewController: nightscoutVC)
-
-        let style = Storage.shared.appearanceMode.value.userInterfaceStyle
-        nightscoutVC.overrideUserInterfaceStyle = style
-        navController.overrideUserInterfaceStyle = style
-
-        nightscoutVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
-            image: UIImage(systemName: "checkmark"),
-            style: .plain,
-            target: self,
-            action: #selector(dismissModal)
-        )
-        nightscoutVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
-
-        navController.modalPresentationStyle = .fullScreen
-        present(navController, animated: true)
+        nightscoutVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+        navigationController?.pushViewController(nightscoutVC, animated: true)
+        nightscoutVC.navigationItem.largeTitleDisplayMode = .never
     }
 
     private func openSnoozer() {
         let storyboard = UIStoryboard(name: "Main", bundle: nil)
         let snoozerVC = storyboard.instantiateViewController(withIdentifier: "SnoozerViewController")
-        let navController = UINavigationController(rootViewController: snoozerVC)
-
-        let style = Storage.shared.appearanceMode.value.userInterfaceStyle
-        snoozerVC.overrideUserInterfaceStyle = style
-        navController.overrideUserInterfaceStyle = style
-
-        snoozerVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
-            image: UIImage(systemName: "checkmark"),
-            style: .plain,
-            target: self,
-            action: #selector(dismissModal)
-        )
-        snoozerVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
-
-        navController.modalPresentationStyle = .fullScreen
-        present(navController, animated: true)
+        snoozerVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+        navigationController?.pushViewController(snoozerVC, animated: true)
+        snoozerVC.navigationItem.largeTitleDisplayMode = .never
     }
 
     private func openTreatments() {
-        let treatmentsVC = UIHostingController(rootView: TreatmentsView())
-        let navController = UINavigationController(rootViewController: treatmentsVC)
-
-        let style = Storage.shared.appearanceMode.value.userInterfaceStyle
-        treatmentsVC.overrideUserInterfaceStyle = style
-        navController.overrideUserInterfaceStyle = style
-
-        treatmentsVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
-            image: UIImage(systemName: "checkmark"),
-            style: .plain,
-            target: self,
-            action: #selector(dismissModal)
-        )
-        treatmentsVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
-
-        navController.modalPresentationStyle = .fullScreen
-        present(navController, animated: true)
+        let treatmentsView = TreatmentsView(onBack: { [weak self] in
+            self?.navigationController?.popViewController(animated: true)
+        })
+        let treatmentsVC = NavBarHidingHostingController(rootView: treatmentsView)
+        treatmentsVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+        navigationController?.pushViewController(treatmentsVC, animated: true)
     }
 
     private func openAggregatedStats() {
@@ -380,51 +317,25 @@ class MoreMenuViewController: UIViewController {
         let statsVC = UIHostingController(
             rootView: AggregatedStatsView(viewModel: AggregatedStatsViewModel(mainViewController: mainVC))
         )
-        let navController = UINavigationController(rootViewController: statsVC)
-
-        let style = Storage.shared.appearanceMode.value.userInterfaceStyle
-        statsVC.overrideUserInterfaceStyle = style
-        navController.overrideUserInterfaceStyle = style
-
-        statsVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
-            image: UIImage(systemName: "checkmark"),
-            style: .plain,
-            target: self,
-            action: #selector(dismissModal)
-        )
-        statsVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
-
-        navController.modalPresentationStyle = .fullScreen
-        present(navController, animated: true)
+        statsVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+        navigationController?.pushViewController(statsVC, animated: true)
     }
 
     private func openHome() {
-        let homeModalView = HomeModalView()
-        let hostingController = UIHostingController(rootView: homeModalView)
-        hostingController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
-        hostingController.modalPresentationStyle = .fullScreen
-        present(hostingController, animated: true)
+        let storyboard = UIStoryboard(name: "Main", bundle: nil)
+        guard let mainVC = storyboard.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController else { return }
+        mainVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+        mainVC.navigationItem.largeTitleDisplayMode = .never
+        navigationController?.pushViewController(mainVC, animated: true)
     }
 
     private func openViewLog() {
-        let logVC = UIHostingController(rootView: LogView())
-        logVC.title = "Log"
-        let navController = UINavigationController(rootViewController: logVC)
-
-        let style = Storage.shared.appearanceMode.value.userInterfaceStyle
-        logVC.overrideUserInterfaceStyle = style
-        navController.overrideUserInterfaceStyle = style
-
-        logVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
-            image: UIImage(systemName: "checkmark"),
-            style: .plain,
-            target: self,
-            action: #selector(dismissModal)
-        )
-        logVC.navigationItem.rightBarButtonItem?.tintColor = .systemBlue
-
-        navController.modalPresentationStyle = .fullScreen
-        present(navController, animated: true)
+        let logView = LogView(onBack: { [weak self] in
+            self?.navigationController?.popViewController(animated: true)
+        })
+        let logVC = NavBarHidingHostingController(rootView: logView)
+        logVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+        navigationController?.pushViewController(logVC, animated: true)
     }
 
     private func shareLogs() {
@@ -472,9 +383,17 @@ class MoreMenuViewController: UIViewController {
         fallbackMainViewController = mainVC
         return mainVC
     }
+}
+
+// MARK: - NavBarHidingHostingController
 
-    @objc private func dismissModal() {
-        dismiss(animated: true)
+/// A UIHostingController subclass that hides the UIKit navigation bar.
+/// Used for SwiftUI views that have their own NavigationStack/NavigationView
+/// to prevent double navigation bars when pushed onto a UINavigationController.
+private class NavBarHidingHostingController<Content: View>: UIHostingController<Content> {
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        navigationController?.setNavigationBarHidden(true, animated: animated)
     }
 }