Jonas Björkert 1 anno fa
parent
commit
66fcff8a03

+ 12 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -41,6 +41,9 @@
 		DD16AF112C997B4600FB655A /* LoadingButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF102C997B4600FB655A /* LoadingButtonView.swift */; };
 		DD1A97142D4294A5000DDC11 /* AdvancedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A97132D4294A4000DDC11 /* AdvancedSettingsView.swift */; };
 		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 */; };
@@ -415,6 +418,9 @@
 		DD16AF102C997B4600FB655A /* LoadingButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonView.swift; sourceTree = "<group>"; };
 		DD1A97132D4294A4000DDC11 /* AdvancedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsView.swift; sourceTree = "<group>"; };
 		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>"; };
@@ -863,6 +869,7 @@
 		DD1A97122D429495000DDC11 /* Settings */ = {
 			isa = PBXGroup;
 			children = (
+				DD1D52BC2E1EB62E00432050 /* TabCustomizationSettingsView.swift */,
 				DD83164F2DE4E635004467AA /* SettingsMenuView.swift */,
 				DD2C2E552D3C3913006413A5 /* DexcomSettingsView.swift */,
 				DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */,
@@ -1456,6 +1463,7 @@
 		FCC688542489367300A0279D /* Helpers */ = {
 			isa = PBXGroup;
 			children = (
+				DD1D52B82E1EB5DC00432050 /* TabPosition.swift */,
 				DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */,
 				DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */,
 				DD7B0D432D730A320063DCB6 /* CycleHelper.swift */,
@@ -1486,6 +1494,7 @@
 		FCC68871248A736700A0279D /* ViewControllers */ = {
 			isa = PBXGroup;
 			children = (
+				DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */,
 				DD12D4842E1705D9004E0112 /* AlarmViewController.swift */,
 				FC97881B2485969B00A7906C /* MainViewController.swift */,
 				FC97881D2485969B00A7906C /* NightScoutViewController.swift */,
@@ -1938,6 +1947,7 @@
 				FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */,
 				FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */,
 				DDF6999E2C5AAA640058A8D9 /* ErrorMessageView.swift in Sources */,
+				DD1D52BB2E1EB60B00432050 /* MoreMenuViewController.swift in Sources */,
 				DD4878152C7B75230048F05C /* MealView.swift in Sources */,
 				FC16A97F249969E2003D6245 /* Graphs.swift in Sources */,
 				FC8589BF252B54F500C8FC73 /* Mobileprovision.swift in Sources */,
@@ -1993,6 +2003,7 @@
 				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 */,
@@ -2034,6 +2045,7 @@
 				DDC6CA412DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift in Sources */,
 				DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */,
 				DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */,
+				DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */,
 				DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */,
 				DD0650F52DCF303F004D3B41 /* AlarmStepperSection.swift in Sources */,
 				DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */,

+ 3 - 3
LoopFollow/Application/Base.lproj/Main.storyboard

@@ -321,7 +321,7 @@
         <!--Snoozer-->
         <scene sceneID="gSd-1V-vbL">
             <objects>
-                <viewController id="e0E-cx-0Wr" customClass="SnoozerViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
+                <viewController storyboardIdentifier="SnoozerViewController" id="e0E-cx-0Wr" customClass="SnoozerViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
                     <view key="view" contentMode="scaleToFill" id="oqe-Oz-Hai">
                         <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -337,7 +337,7 @@
         <!--Nightscout-->
         <scene sceneID="wg7-f3-ORb">
             <objects>
-                <viewController id="8rJ-Kc-sve" customClass="NightscoutViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
+                <viewController storyboardIdentifier="NightscoutViewController" id="8rJ-Kc-sve" customClass="NightscoutViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
                     <view key="view" contentMode="scaleToFill" id="QS5-Rx-YEW">
                         <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -405,7 +405,7 @@
         <!--Settings-->
         <scene sceneID="ORV-wr-Fd3">
             <objects>
-                <viewController id="hDv-qK-Udb" customClass="SettingsViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
+                <viewController storyboardIdentifier="SettingsViewController" id="hDv-qK-Udb" customClass="SettingsViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
                     <view key="view" contentMode="scaleToFill" id="ljg-dW-5uR">
                         <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>

+ 19 - 0
LoopFollow/Helpers/TabPosition.swift

@@ -0,0 +1,19 @@
+// LoopFollow
+// TabPosition.swift
+// Created by Jonas Björkert.
+
+enum TabPosition: String, CaseIterable, Codable {
+    case position2
+    case position4
+    case more
+    case disabled
+
+    var displayName: String {
+        switch self {
+        case .position2: return "Tab 2"
+        case .position4: return "Tab 4"
+        case .more: return "More Menu"
+        case .disabled: return "Hidden"
+        }
+    }
+}

+ 8 - 0
LoopFollow/Settings/SettingsMenuView.swift

@@ -44,6 +44,12 @@ struct SettingsMenuView: View {
                         settingsPath.value.append(Sheet.graph)
                     }
 
+                    NavigationRow(title: "Tab Customization",
+                                  icon: "rectangle.3.group")
+                    {
+                        settingsPath.value.append(Sheet.tabCustomization)
+                    }
+
                     if !nightscoutURL.value.isEmpty {
                         NavigationRow(title: "Information Display Settings",
                                       icon: "info.circle")
@@ -226,6 +232,7 @@ private enum Sheet: Hashable, Identifiable {
     case calendar, contact
     case advanced
     case viewLog
+    case tabCustomization
 
     var id: Self { self }
 
@@ -245,6 +252,7 @@ private enum Sheet: Hashable, Identifiable {
         case .contact: ContactSettingsView(viewModel: .init())
         case .advanced: AdvancedSettingsView(viewModel: .init())
         case .viewLog: LogView(viewModel: .init())
+        case .tabCustomization: TabCustomizationSettingsView()
         }
     }
 }

+ 97 - 0
LoopFollow/Settings/TabCustomizationSettingsView.swift

@@ -0,0 +1,97 @@
+// 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()
+        }
+    }
+}

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

@@ -5,6 +5,18 @@
 import Foundation
 
 extension Storage {
+    func migrateStep2() {
+        // Migrate from old system to new position-based system
+        if remoteType.value != .none {
+            remotePosition.value = .position2
+            alarmsPosition.value = .more
+        } else {
+            alarmsPosition.value = .position2
+            remotePosition.value = .more
+        }
+        nightscoutPosition.value = .position4
+    }
+
     func migrateStep1() {
         Storage.shared.url.value = ObservableUserDefaults.shared.old_url.value
         Storage.shared.device.value = ObservableUserDefaults.shared.old_device.value

+ 4 - 0
LoopFollow/Storage/Storage.swift

@@ -162,6 +162,10 @@ class Storage {
 
     var lastLoopingChecked = StorageValue<Date?>(key: "lastLoopingChecked", defaultValue: nil)
 
+    var alarmsPosition = StorageValue<TabPosition>(key: "alarmsPosition", defaultValue: .position2)
+    var remotePosition = StorageValue<TabPosition>(key: "remotePosition", defaultValue: .more)
+    var nightscoutPosition = StorageValue<TabPosition>(key: "nightscoutPosition", defaultValue: .position4)
+
     static let shared = Storage()
     private init() {}
 }

+ 126 - 47
LoopFollow/ViewControllers/MainViewController.swift

@@ -129,6 +129,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
             Storage.shared.migrationStep.value = 1
         }
 
+        if Storage.shared.migrationStep.value < 2 {
+            Storage.shared.migrateStep2()
+            Storage.shared.migrationStep.value = 2
+        }
+
         // Synchronize info types to ensure arrays are the correct size
         synchronizeInfoTypes()
 
@@ -278,64 +283,132 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
             }
             .store(in: &cancellables)
 
-        Storage.shared.remoteType.$value
+        Storage.shared.alarmsPosition.$value
             .receive(on: DispatchQueue.main)
-            .sink { [weak self] remoteType in
-                if remoteType == .none {
-                    // If remote is disabled, show the Alarms tab.
-                    self?.updateSecondTab(to: .alarms)
-                } else {
-                    // Otherwise, show the Remote tab.
-                    self?.updateSecondTab(to: .remote)
-                }
+            .sink { [weak self] _ in
+                self?.setupTabBar()
+            }
+            .store(in: &cancellables)
+
+        Storage.shared.remotePosition.$value
+            .receive(on: DispatchQueue.main)
+            .sink { [weak self] _ in
+                self?.setupTabBar()
+            }
+            .store(in: &cancellables)
+
+        Storage.shared.nightscoutPosition.$value
+            .receive(on: DispatchQueue.main)
+            .sink { [weak self] _ in
+                self?.setupTabBar()
             }
             .store(in: &cancellables)
 
         Storage.shared.url.$value
             .receive(on: DispatchQueue.main)
-            .sink { [weak self] value in
-                self?.tabBarController?.tabBar.items?[3].isEnabled = !value.isEmpty
+            .sink { [weak self] _ in
+                self?.updateNightscoutTabState()
             }
             .store(in: &cancellables)
 
         updateQuickActions()
+        setupTabBar()
 
         speechSynthesizer.delegate = self
     }
 
-    private func updateSecondTab(to tab: SecondTab) {
-        guard let tabBarController = tabBarController,
-              var viewControllers = tabBarController.viewControllers,
-              viewControllers.count > 1
-        else {
-            return
+    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 &&
+            tabBarController.viewControllers?.last is MoreMenuViewController
+        let willHaveMoreTab = hasItemsInMore()
+
+        // If we're in More tab and it's going away, handle the transition
+        if wasInMoreTab && !willHaveMoreTab {
+            // First dismiss any modal that might be presented
+            if let moreVC = tabBarController.viewControllers?.last as? MoreMenuViewController,
+               let presented = moreVC.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)
+                }
+                return
+            } else {
+                // No modal presented, just switch to home
+                tabBarController.selectedIndex = 0
+            }
         }
 
+        completeTabBarSetup(tabBarController: tabBarController, willHaveMoreTab: willHaveMoreTab)
+    }
+
+    private func completeTabBarSetup(tabBarController: UITabBarController, willHaveMoreTab: Bool) {
         let storyboard = UIStoryboard(name: "Main", bundle: nil)
-        let newViewController: UIViewController
-        let newTabBarItem: UITabBarItem
-
-        switch tab {
-        case .remote:
-            newViewController = storyboard.instantiateViewController(withIdentifier: "RemoteViewController")
-            newTabBarItem = UITabBarItem(
-                title: "Remote",
-                image: UIImage(systemName: "antenna.radiowaves.left.and.right"),
-                tag: 1
-            )
-        case .alarms:
-            newViewController = storyboard.instantiateViewController(withIdentifier: "AlarmViewController")
-            newTabBarItem = UITabBarItem(
-                title: "Alarms",
-                image: UIImage(systemName: "alarm"),
-                tag: 1
-            )
-        }
-
-        newViewController.tabBarItem = newTabBarItem
-        viewControllers[1] = newViewController
+        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)
+        }
 
         tabBarController.setViewControllers(viewControllers, animated: false)
+        updateNightscoutTabState()
+    }
+
+    private func createViewController(for position: TabPosition, storyboard: UIStoryboard) -> UIViewController? {
+        if Storage.shared.alarmsPosition.value == position {
+            let vc = storyboard.instantiateViewController(withIdentifier: "AlarmViewController")
+            vc.tabBarItem = UITabBarItem(title: "Alarms", image: UIImage(systemName: "alarm"), tag: position == .position2 ? 1 : 3)
+            return vc
+        }
+
+        if Storage.shared.remotePosition.value == position {
+            let vc = storyboard.instantiateViewController(withIdentifier: "RemoteViewController")
+            vc.tabBarItem = UITabBarItem(title: "Remote", image: UIImage(systemName: "antenna.radiowaves.left.and.right"), tag: position == .position2 ? 1 : 3)
+            return vc
+        }
+
+        if Storage.shared.nightscoutPosition.value == position {
+            let vc = storyboard.instantiateViewController(withIdentifier: "NightscoutViewController")
+            vc.tabBarItem = UITabBarItem(title: "Nightscout", image: UIImage(systemName: "safari"), tag: position == .position2 ? 1 : 3)
+            return vc
+        }
+
+        return nil
+    }
+
+    private func hasItemsInMore() -> Bool {
+        return Storage.shared.alarmsPosition.value == .more ||
+            Storage.shared.remotePosition.value == .more ||
+            Storage.shared.nightscoutPosition.value == .more
     }
 
     // Update the Home Screen Quick Action for toggling the "Speak BG" feature based on the current speakBG setting.
@@ -537,12 +610,23 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         return String(format: "%02d:%02d", hours, minutes)
     }
 
+    private func updateNightscoutTabState() {
+        guard let tabBarController = tabBarController,
+              let viewControllers = tabBarController.viewControllers else { return }
+
+        let isNightscoutEnabled = !Storage.shared.url.value.isEmpty
+
+        for (index, vc) in viewControllers.enumerated() {
+            if vc is NightscoutViewController {
+                tabBarController.tabBar.items?[index].isEnabled = isNightscoutEnabled
+            }
+        }
+    }
+
     func showHideNSDetails() {
         var isHidden = false
-        var isEnabled = true
         if !IsNightscoutEnabled() {
             isHidden = true
-            isEnabled = false
         }
 
         LoopStatusLabel.isHidden = isHidden
@@ -557,12 +641,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
             infoTable.isHidden = true
         }
 
-        if IsNightscoutEnabled() {
-            isEnabled = true
-        }
-
-        guard let nightscoutTab = tabBarController?.tabBar.items![3] else { return }
-        nightscoutTab.isEnabled = isEnabled
+        updateNightscoutTabState()
     }
 
     func updateBadge(val: Int) {

+ 215 - 0
LoopFollow/ViewControllers/MoreMenuViewController.swift

@@ -0,0 +1,215 @@
+// LoopFollow
+// MoreMenuViewController.swift
+// Created by Jonas Björkert.
+
+import SwiftUI
+import UIKit
+
+class MoreMenuViewController: UIViewController {
+    private var tableView: UITableView!
+
+    struct MenuItem {
+        let title: String
+        let icon: String
+        let action: () -> Void
+    }
+
+    private var menuItems: [MenuItem] = []
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        title = "More"
+        view.backgroundColor = .systemBackground
+
+        // Apply dark mode if needed
+        if Storage.shared.forceDarkMode.value {
+            overrideUserInterfaceStyle = .dark
+        }
+
+        setupTableView()
+        updateMenuItems()
+    }
+
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        updateMenuItems()
+        tableView.reloadData()
+    }
+
+    private func setupTableView() {
+        tableView = UITableView(frame: view.bounds, style: .insetGrouped)
+        tableView.translatesAutoresizingMaskIntoConstraints = false
+        tableView.delegate = self
+        tableView.dataSource = self
+        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
+
+        view.addSubview(tableView)
+
+        NSLayoutConstraint.activate([
+            tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+        ])
+    }
+
+    private func updateMenuItems() {
+        menuItems = []
+
+        // Always add Settings
+        menuItems.append(MenuItem(
+            title: "Settings",
+            icon: "gear",
+            action: { [weak self] in
+                self?.openSettings()
+            }
+        ))
+
+        // Add items based on their positions
+        if Storage.shared.alarmsPosition.value == .more {
+            menuItems.append(MenuItem(
+                title: "Alarms",
+                icon: "alarm",
+                action: { [weak self] in
+                    self?.openAlarms()
+                }
+            ))
+        }
+
+        if Storage.shared.remotePosition.value == .more {
+            menuItems.append(MenuItem(
+                title: "Remote",
+                icon: "antenna.radiowaves.left.and.right",
+                action: { [weak self] in
+                    self?.openRemote()
+                }
+            ))
+        }
+
+        if Storage.shared.nightscoutPosition.value == .more {
+            menuItems.append(MenuItem(
+                title: "Nightscout",
+                icon: "safari",
+                action: { [weak self] in
+                    self?.openNightscout()
+                }
+            ))
+        }
+    }
+
+    private func openSettings() {
+        let settingsVC = UIHostingController(rootView: SettingsMenuView())
+        let navController = UINavigationController(rootViewController: settingsVC)
+
+        // Apply dark mode if needed
+        if Storage.shared.forceDarkMode.value {
+            settingsVC.overrideUserInterfaceStyle = .dark
+            navController.overrideUserInterfaceStyle = .dark
+        }
+
+        // Add a close button
+        settingsVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
+            barButtonSystemItem: .done,
+            target: self,
+            action: #selector(dismissModal)
+        )
+
+        navController.modalPresentationStyle = .fullScreen
+        present(navController, animated: true)
+    }
+
+    private func openAlarms() {
+        let storyboard = UIStoryboard(name: "Main", bundle: nil)
+        let alarmsVC = storyboard.instantiateViewController(withIdentifier: "AlarmViewController")
+        let navController = UINavigationController(rootViewController: alarmsVC)
+
+        // Apply dark mode if needed
+        if Storage.shared.forceDarkMode.value {
+            alarmsVC.overrideUserInterfaceStyle = .dark
+            navController.overrideUserInterfaceStyle = .dark
+        }
+
+        // Add a close button
+        alarmsVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
+            barButtonSystemItem: .done,
+            target: self,
+            action: #selector(dismissModal)
+        )
+
+        navController.modalPresentationStyle = .fullScreen
+        present(navController, animated: true)
+    }
+
+    private func openRemote() {
+        let storyboard = UIStoryboard(name: "Main", bundle: nil)
+        let remoteVC = storyboard.instantiateViewController(withIdentifier: "RemoteViewController")
+        let navController = UINavigationController(rootViewController: remoteVC)
+
+        // Apply dark mode if needed
+        if Storage.shared.forceDarkMode.value {
+            remoteVC.overrideUserInterfaceStyle = .dark
+            navController.overrideUserInterfaceStyle = .dark
+        }
+
+        // Add a close button
+        remoteVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
+            barButtonSystemItem: .done,
+            target: self,
+            action: #selector(dismissModal)
+        )
+
+        navController.modalPresentationStyle = .fullScreen
+        present(navController, animated: true)
+    }
+
+    private func openNightscout() {
+        let storyboard = UIStoryboard(name: "Main", bundle: nil)
+        let nightscoutVC = storyboard.instantiateViewController(withIdentifier: "NightscoutViewController")
+        let navController = UINavigationController(rootViewController: nightscoutVC)
+
+        // Apply dark mode if needed
+        if Storage.shared.forceDarkMode.value {
+            nightscoutVC.overrideUserInterfaceStyle = .dark
+            navController.overrideUserInterfaceStyle = .dark
+        }
+
+        // Add a close button
+        nightscoutVC.navigationItem.rightBarButtonItem = UIBarButtonItem(
+            barButtonSystemItem: .done,
+            target: self,
+            action: #selector(dismissModal)
+        )
+
+        navController.modalPresentationStyle = .fullScreen
+        present(navController, animated: true)
+    }
+
+    @objc private func dismissModal() {
+        dismiss(animated: true)
+    }
+}
+
+extension MoreMenuViewController: UITableViewDataSource, UITableViewDelegate {
+    func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
+        return menuItems.count
+    }
+
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
+        let item = menuItems[indexPath.row]
+
+        var config = cell.defaultContentConfiguration()
+        config.text = item.title
+        config.image = UIImage(systemName: item.icon)
+        cell.contentConfiguration = config
+        cell.accessoryType = .disclosureIndicator
+
+        return cell
+    }
+
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        tableView.deselectRow(at: indexPath, animated: true)
+        menuItems[indexPath.row].action()
+    }
+}