From 66fcff8a03efdd291a4d31d1fc78cb30583c6873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 17 Jul 2025 16:43:35 +0200 Subject: [PATCH 1/4] More menu --- LoopFollow.xcodeproj/project.pbxproj | 12 + .../Application/Base.lproj/Main.storyboard | 6 +- LoopFollow/Helpers/TabPosition.swift | 19 ++ LoopFollow/Settings/SettingsMenuView.swift | 8 + .../TabCustomizationSettingsView.swift | 97 ++++++++ LoopFollow/Storage/Storage+Migrate.swift | 12 + LoopFollow/Storage/Storage.swift | 4 + .../ViewControllers/MainViewController.swift | 173 ++++++++++---- .../MoreMenuViewController.swift | 215 ++++++++++++++++++ 9 files changed, 496 insertions(+), 50 deletions(-) create mode 100644 LoopFollow/Helpers/TabPosition.swift create mode 100644 LoopFollow/Settings/TabCustomizationSettingsView.swift create mode 100644 LoopFollow/ViewControllers/MoreMenuViewController.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 0087f389e..500e2cecb 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/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 = ""; }; DD1A97132D4294A4000DDC11 /* AdvancedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsView.swift; sourceTree = ""; }; DD1A97152D4294B2000DDC11 /* AdvancedSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsViewModel.swift; sourceTree = ""; }; + DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = ""; }; + DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuViewController.swift; sourceTree = ""; }; + DD1D52BC2E1EB62E00432050 /* TabCustomizationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationSettingsView.swift; sourceTree = ""; }; DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsView.swift; sourceTree = ""; }; DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsViewModel.swift; sourceTree = ""; }; DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSettingsViewModel.swift; sourceTree = ""; }; @@ -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 */, diff --git a/LoopFollow/Application/Base.lproj/Main.storyboard b/LoopFollow/Application/Base.lproj/Main.storyboard index 2278db6db..a0acee396 100644 --- a/LoopFollow/Application/Base.lproj/Main.storyboard +++ b/LoopFollow/Application/Base.lproj/Main.storyboard @@ -321,7 +321,7 @@ - + @@ -337,7 +337,7 @@ - + @@ -405,7 +405,7 @@ - + diff --git a/LoopFollow/Helpers/TabPosition.swift b/LoopFollow/Helpers/TabPosition.swift new file mode 100644 index 000000000..e060ad1d3 --- /dev/null +++ b/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" + } + } +} diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index d8a52ccac..0dc15cd75 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/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() } } } diff --git a/LoopFollow/Settings/TabCustomizationSettingsView.swift b/LoopFollow/Settings/TabCustomizationSettingsView.swift new file mode 100644 index 000000000..a65f5324f --- /dev/null +++ b/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() + } + } +} diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 5af3eb8ac..16c4a2b9b 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/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 diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 49050f9a0..9d65aeb6b 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -162,6 +162,10 @@ class Storage { var lastLoopingChecked = StorageValue(key: "lastLoopingChecked", defaultValue: nil) + var alarmsPosition = StorageValue(key: "alarmsPosition", defaultValue: .position2) + var remotePosition = StorageValue(key: "remotePosition", defaultValue: .more) + var nightscoutPosition = StorageValue(key: "nightscoutPosition", defaultValue: .position4) + static let shared = Storage() private init() {} } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index c698cf53c..61f778a57 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/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) { diff --git a/LoopFollow/ViewControllers/MoreMenuViewController.swift b/LoopFollow/ViewControllers/MoreMenuViewController.swift new file mode 100644 index 000000000..e9f2fe847 --- /dev/null +++ b/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() + } +} From 6deff8b0976f8ad6a37a27c76295382d28e67abe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 17 Jul 2025 21:40:34 +0200 Subject: [PATCH 2/4] Fix for resetting settings navigation --- LoopFollow/ViewControllers/MoreMenuViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LoopFollow/ViewControllers/MoreMenuViewController.swift b/LoopFollow/ViewControllers/MoreMenuViewController.swift index e9f2fe847..f0bb38828 100644 --- a/LoopFollow/ViewControllers/MoreMenuViewController.swift +++ b/LoopFollow/ViewControllers/MoreMenuViewController.swift @@ -35,6 +35,7 @@ class MoreMenuViewController: UIViewController { super.viewWillAppear(animated) updateMenuItems() tableView.reloadData() + Observable.shared.settingsPath.set(NavigationPath()) } private func setupTableView() { From d9269da7b09c2c9edd2dad5f090aed8036087594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 18 Jul 2025 21:35:42 +0200 Subject: [PATCH 3/4] Improved tab configuration --- LoopFollow.xcodeproj/project.pbxproj | 8 +- LoopFollow/Settings/SettingsMenuView.swift | 47 ++++- .../Settings/TabCustomizationModal.swift | 168 ++++++++++++++++++ .../TabCustomizationSettingsView.swift | 97 ---------- .../ViewControllers/MainViewController.swift | 76 ++++++-- 5 files changed, 277 insertions(+), 119 deletions(-) create mode 100644 LoopFollow/Settings/TabCustomizationModal.swift delete mode 100644 LoopFollow/Settings/TabCustomizationSettingsView.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 500e2cecb..69d875dd1 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/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 = ""; }; DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = ""; }; DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuViewController.swift; sourceTree = ""; }; - DD1D52BC2E1EB62E00432050 /* TabCustomizationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationSettingsView.swift; sourceTree = ""; }; DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsView.swift; sourceTree = ""; }; DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsViewModel.swift; sourceTree = ""; }; DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomSettingsViewModel.swift; sourceTree = ""; }; @@ -500,6 +499,7 @@ DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+SortDirection.swift"; sourceTree = ""; }; DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+timeUnit.swift"; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; + DD8060DA2E2ACE5900626B91 /* TabCustomizationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationModal.swift; sourceTree = ""; }; DD8316172DE3633D004467AA /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; DD8316432DE47CA9004467AA /* BGPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGPicker.swift; sourceTree = ""; }; DD8316452DE49B09004467AA /* GraphSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsView.swift; sourceTree = ""; }; @@ -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 */, diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 0dc15cd75..37024ae4f 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/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() } } } diff --git a/LoopFollow/Settings/TabCustomizationModal.swift b/LoopFollow/Settings/TabCustomizationModal.swift new file mode 100644 index 000000000..02d0d5f4d --- /dev/null +++ b/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, 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() + } + } +} diff --git a/LoopFollow/Settings/TabCustomizationSettingsView.swift b/LoopFollow/Settings/TabCustomizationSettingsView.swift deleted file mode 100644 index a65f5324f..000000000 --- a/LoopFollow/Settings/TabCustomizationSettingsView.swift +++ /dev/null @@ -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() - } - } -} diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 61f778a57..38c797016 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/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) { From c680029141787553be20557144ab947c49b39293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 18 Jul 2025 23:21:50 +0200 Subject: [PATCH 4/4] Fix for snoozer tab position --- .../ViewControllers/MainViewController.swift | 46 +++++-------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 38c797016..6d55114ec 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -228,9 +228,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele /// When an alarm is triggered, go to the snoozer tab Observable.shared.currentAlarm.$value .receive(on: DispatchQueue.main) - .compactMap { $0 } /// Ignore nil + .compactMap { $0 } .sink { [weak self] _ in - self?.tabBarController?.selectedIndex = 2 + if let snoozerIndex = self?.getSnoozerTabIndex() { + self?.tabBarController?.selectedIndex = snoozerIndex + } } .store(in: &cancellables) @@ -394,41 +396,17 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele updateNightscoutTabState() } - private func completeTabBarSetup(tabBarController: UITabBarController, willHaveMoreTab: Bool) { - 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) - } + private func getSnoozerTabIndex() -> Int? { + guard let tabBarController = tabBarController, + let viewControllers = tabBarController.viewControllers else { return nil } - // 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) + for (index, vc) in viewControllers.enumerated() { + if let _ = vc as? SnoozerViewController { + return index + } } - tabBarController.setViewControllers(viewControllers, animated: false) - updateNightscoutTabState() + return nil } private func createViewController(for position: TabPosition, storyboard: UIStoryboard) -> UIViewController? {