Jonas Björkert 1 rok pred
rodič
commit
6e728719df

+ 4 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -126,6 +126,7 @@
 		DD83164A2DE4C504004467AA /* SettingsStepperRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316492DE4C504004467AA /* SettingsStepperRow.swift */; };
 		DD83164C2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */; };
 		DD83164E2DE4E093004467AA /* WatchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD83164D2DE4E093004467AA /* WatchSettingsView.swift */; };
+		DD8316502DE4E635004467AA /* SettingsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD83164F2DE4E635004467AA /* SettingsMenuView.swift */; };
 		DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; };
 		DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; };
 		DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */; };
@@ -488,6 +489,7 @@
 		DD8316492DE4C504004467AA /* SettingsStepperRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStepperRow.swift; sourceTree = "<group>"; };
 		DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BinaryFloatingPoint+localized.swift"; sourceTree = "<group>"; };
 		DD83164D2DE4E093004467AA /* WatchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsView.swift; sourceTree = "<group>"; };
+		DD83164F2DE4E635004467AA /* SettingsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMenuView.swift; sourceTree = "<group>"; };
 		DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = "<group>"; };
 		DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = "<group>"; };
 		DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientExtension.swift; sourceTree = "<group>"; };
@@ -841,6 +843,7 @@
 		DD1A97122D429495000DDC11 /* Settings */ = {
 			isa = PBXGroup;
 			children = (
+				DD83164F2DE4E635004467AA /* SettingsMenuView.swift */,
 				DD83164D2DE4E093004467AA /* WatchSettingsView.swift */,
 				DD8316452DE49B09004467AA /* GraphSettingsView.swift */,
 				DD1A97152D4294B2000DDC11 /* AdvancedSettingsViewModel.swift */,
@@ -2005,6 +2008,7 @@
 				DD7F4C132DD51FD500D449E9 /* TempTargetEndCondition.swift in Sources */,
 				DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */,
 				DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */,
+				DD8316502DE4E635004467AA /* SettingsMenuView.swift in Sources */,
 				DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */,
 				DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */,
 				DD8316482DE49EE5004467AA /* Storage+Migrate.swift in Sources */,

+ 8 - 0
LoopFollow/Helpers/AppVersionManager.swift

@@ -7,6 +7,14 @@ import Foundation
 class AppVersionManager {
     private let githubService = GitHubService()
 
+    func checkForNewVersionAsync() async -> (latest: String?, isNewer: Bool, isBlacklisted: Bool) {
+        await withCheckedContinuation { cont in
+            checkForNewVersion { latest, newer, blacklisted in
+                cont.resume(returning: (latest, newer, blacklisted))
+            }
+        }
+    }
+
     /// Checks for the availability of a new app version and if the current version is blacklisted.
     /// - Parameter completion: Returns latest version, a boolean for newer version existence, and blacklist status.
     /// Usage: `versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in ... }`

+ 260 - 0
LoopFollow/Settings/SettingsMenuView.swift

@@ -0,0 +1,260 @@
+// LoopFollow
+// SettingsMenuView.swift
+// Created by Jonas Björkert on 2025-05-26.
+
+import SwiftUI
+
+struct SettingsMenuView: View {
+    // MARK: – Call-backs -----------------------------------------------------
+
+    let onNightscoutVisibilityChange: (_ enabled: Bool) -> Void
+
+    // MARK: – Local state ----------------------------------------------------
+
+    @State private var sheet: Sheet?
+    @State private var latestVersion: String?
+    @State private var versionTint: Color = .secondary
+
+    // MARK: – Body -----------------------------------------------------------
+
+    var body: some View {
+        NavigationStack {
+            List {
+                // ────────────── Data settings ──────────────
+                dataSection
+
+                // ────────────── App settings ──────────────
+                Section("App Settings") {
+                    navRow(title: "Background Refresh Settings",
+                           icon: "arrow.clockwise",
+                           destination: .backgroundRefresh)
+
+                    navRow(title: "General Settings",
+                           icon: "gearshape",
+                           destination: .general)
+
+                    navRow(title: "Graph Settings",
+                           icon: "chart.xyaxis.line",
+                           destination: .graph)
+
+                    if IsNightscoutEnabled() {
+                        navRow(title: "Information Display Settings",
+                               icon: "info.circle",
+                               destination: .infoDisplay)
+                    }
+                }
+
+                // ────────────── Alarms ──────────────
+                Section {
+                    navRow(title: "Alarms",
+                           icon: "bell",
+                           destination: .alarmsList)
+
+                    navRow(title: "Alarm Settings",
+                           icon: "bell.badge",
+                           destination: .alarmSettings)
+                }
+
+                // ────────────── Integrations ──────────────
+                Section("Integrations") {
+                    navRow(title: "Calendar",
+                           icon: "calendar",
+                           destination: .calendar)
+
+                    navRow(title: "Contact",
+                           icon: "envelope",
+                           destination: .contact)
+                }
+
+                // ────────────── Advanced / Logs ──────────────
+                Section("Advanced Settings") {
+                    navRow(title: "Advanced Settings",
+                           icon: "exclamationmark.shield",
+                           destination: .advanced)
+                }
+
+                Section("Logging") {
+                    navRow(title: "View Log",
+                           icon: "doc.text.magnifyingglass",
+                           destination: .viewLog)
+
+                    Button {
+                        shareLogs()
+                    } label: {
+                        Label("Share Logs", systemImage: "square.and.arrow.up")
+                    }
+                    .accessibilityIdentifier("ShareLogsButton")
+                }
+
+                // ────────────── Community ──────────────
+                Section("Community") {
+                    Link(destination: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!) {
+                        Label("LoopFollow Facebook Group", systemImage: "person.3")
+                    }
+                }
+
+                // ────────────── Build info ──────────────
+                buildInfoSection
+            }
+            .navigationTitle("Settings")
+        }
+        .task { await refreshVersionInfo() }
+        .sheet(item: $sheet) { $0.destination }
+    }
+
+    // MARK: – Section builders ----------------------------------------------
+
+    /// “Data Settings”
+    @ViewBuilder
+    private var dataSection: some View {
+        Section("Data Settings") {
+            Picker("Units",
+                   selection: Binding(
+                       get: { UserDefaultsRepository.units.value },
+                       set: { UserDefaultsRepository.units.value = $0 }
+                   )) {
+                Text("mg/dL").tag("mg/dL")
+                Text("mmol/L").tag("mmol/L")
+            }
+            .pickerStyle(.segmented)
+
+            navRow(title: "Nightscout Settings",
+                   icon: "antenna.radiowaves.left.and.right",
+                   destination: .nightscout)
+
+            navRow(title: "Dexcom Settings",
+                   icon: "waveform.path.ecg",
+                   destination: .dexcom)
+        }
+        .onAppear {
+            onNightscoutVisibilityChange(IsNightscoutEnabled())
+        }
+    }
+
+    /// version / build info
+    @ViewBuilder
+    private var buildInfoSection: some View {
+        let build = BuildDetails.default
+        let ver = AppVersionManager().version()
+
+        Section("Build Information") {
+            keyValue("Version", ver, tint: versionTint)
+            keyValue("Latest version", latestVersion ?? "Fetching…")
+
+            if !(build.isMacApp() || build.isSimulatorBuild()) {
+                keyValue(build.expirationHeaderString,
+                         dateTimeUtils.formattedDate(from: build.calculateExpirationDate()))
+            }
+            keyValue("Built", dateTimeUtils.formattedDate(from: build.buildDate()))
+            keyValue("Branch", build.branchAndSha)
+        }
+    }
+
+    // MARK: – Row helpers ----------------------------------------------------
+
+    /// Standard row with icon, chevron and sheet presentation
+    private func navRow(title: String,
+                        icon: String,
+                        destination: Sheet) -> some View
+    {
+        NavigationLink {
+            destination.destination
+        } label: {
+            Label(title, systemImage: icon)
+        }
+    }
+
+    /// Simple key-value row
+    @ViewBuilder
+    private func keyValue(_ key: String, _ value: String, tint: Color = .secondary) -> some View {
+        HStack {
+            Text(key)
+            Spacer()
+            Text(value).foregroundColor(tint)
+        }
+    }
+
+    // MARK: – Version check --------------------------------------------------
+
+    private func refreshVersionInfo() async {
+        let manager = AppVersionManager()
+        let (latest, newer, blacklisted) = await manager.checkForNewVersionAsync()
+        latestVersion = latest ?? "Unknown"
+
+        // match old colour logic
+        let current = manager.version()
+        versionTint = blacklisted ? .red :
+            newer ? .orange :
+            latest == current ? .green : .secondary
+    }
+
+    // MARK: – Share logs -----------------------------------------------------
+
+    private func shareLogs() {
+        let files = LogManager.shared.logFilesForTodayAndYesterday()
+        guard !files.isEmpty else {
+            UIApplication.shared.topMost?.presentSimpleAlert(
+                title: "No Logs Available",
+                message: "There are no logs to share."
+            )
+            return
+        }
+        let avc = UIActivityViewController(activityItems: files, applicationActivities: nil)
+        UIApplication.shared.topMost?.present(avc, animated: true)
+    }
+}
+
+// MARK: – Sheet routing identical to earlier -------------------------------
+
+private enum Sheet: Identifiable {
+    case nightscout, dexcom
+    case backgroundRefresh
+    case general, graph
+    case infoDisplay
+    case alarmsList, alarmSettings
+    case remote
+    case calendar, contact
+    case advanced
+    case viewLog
+
+    var id: Int { hashValue }
+
+    @ViewBuilder
+    var destination: some View {
+        switch self {
+        case .nightscout: NightscoutSettingsView(viewModel: .init())
+        case .dexcom: DexcomSettingsView(viewModel: .init())
+        case .backgroundRefresh: BackgroundRefreshSettingsView(viewModel: .init())
+        case .general: GeneralSettingsView()
+        case .graph: GraphSettingsView()
+        case .infoDisplay: InfoDisplaySettingsView(viewModel: .init())
+        case .alarmsList: AlarmListView()
+        case .alarmSettings: AlarmSettingsView()
+        case .remote: RemoteSettingsView(viewModel: .init())
+        case .calendar: WatchSettingsView()
+        case .contact: ContactSettingsView(viewModel: .init())
+        case .advanced: AdvancedSettingsView(viewModel: .init())
+        case .viewLog: LogView(viewModel: .init())
+        }
+    }
+}
+
+import UIKit
+
+extension UIApplication {
+    var topMost: UIViewController? {
+        guard var top = keyWindow?.rootViewController else { return nil }
+        while let presented = top.presentedViewController {
+            top = presented
+        }
+        return top
+    }
+}
+
+extension UIViewController {
+    func presentSimpleAlert(title: String, message: String) {
+        let a = UIAlertController(title: title, message: message, preferredStyle: .alert)
+        a.addAction(UIAlertAction(title: "OK", style: .default))
+        present(a, animated: true)
+    }
+}

+ 8 - 7
LoopFollow/ViewControllers/MainViewController.swift

@@ -587,7 +587,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         }
         let direction = bgDirectionGraphic(bgData[bgData.count - 1].direction ?? "")
 
-        var eventStartDate = Date(timeIntervalSince1970: bgData[bgData.count - 1].date)
+        let eventStartDate = Date(timeIntervalSince1970: bgData[bgData.count - 1].date)
         var eventEndDate = eventStartDate.addingTimeInterval(60 * 10)
         var eventTitle = Storage.shared.watchLine1.value
         if Storage.shared.watchLine2.value.count > 1 {
@@ -621,12 +621,12 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         eventTitle = eventTitle.replacingOccurrences(of: "%BASAL%", with: basal)
 
         // Delete Events from last 2 hours and 2 hours in future
-        var deleteStartDate = Date().addingTimeInterval(-60 * 60 * 2)
-        var deleteEndDate = Date().addingTimeInterval(60 * 60 * 2)
+        let deleteStartDate = Date().addingTimeInterval(-60 * 60 * 2)
+        let deleteEndDate = Date().addingTimeInterval(60 * 60 * 2)
         // guard solves for some ios upgrades removing the calendar
         guard let deleteCalendar = store.calendar(withIdentifier: Storage.shared.calendarIdentifier.value) as? EKCalendar else { return }
-        var predicate2 = store.predicateForEvents(withStart: deleteStartDate, end: deleteEndDate, calendars: [deleteCalendar])
-        var eVDelete = store.events(matching: predicate2) as [EKEvent]?
+        let predicate2 = store.predicateForEvents(withStart: deleteStartDate, end: deleteEndDate, calendars: [deleteCalendar])
+        let eVDelete = store.events(matching: predicate2) as [EKEvent]?
         if eVDelete != nil {
             for i in eVDelete! {
                 do {
@@ -638,7 +638,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         }
 
         // Write New Event
-        var event = EKEvent(eventStore: store)
+        let event = EKEvent(eventStore: store)
         event.title = eventTitle
         event.startDate = eventStartDate
         event.endDate = eventEndDate
@@ -649,7 +649,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
 
             lastCalDate = bgData[bgData.count - 1].date
         } catch {
-            LogManager.shared.log(category: .calendar, message: "Error storing to the calendar")
+            let msg = "Error storing to calendar: \(error.localizedDescription) (\(error))"
+            LogManager.shared.log(category: .calendar, message: msg)
         }
     }
 

+ 21 - 430
LoopFollow/ViewControllers/SettingsViewController.swift

@@ -2,449 +2,40 @@
 // SettingsViewController.swift
 // Created by Jon Fawcett on 2020-06-05.
 
-import Eureka
-import EventKit
-import EventKitUI
 import SwiftUI
 import UIKit
 
-class SettingsViewController: FormViewController, NightscoutSettingsViewModelDelegate {
-    var tokenRow: TextRow?
-    var statusLabelRow: LabelRow!
+final class SettingsViewController: UIViewController {
+    // MARK: Stored properties
 
-    func showHideNSDetails() {
-        var isHidden = false
-        var isEnabled = true
-        if !IsNightscoutEnabled() {
-            isHidden = true
-            isEnabled = false
-        }
-
-        if let row1 = form.rowBy(tag: "informationDisplaySettings") as? ButtonRow {
-            row1.hidden = .function(["hide"]) { _ in
-                isHidden
-            }
-            row1.evaluateHidden()
-        }
+    private var host: UIHostingController<SettingsMenuView>!
 
-        if IsNightscoutEnabled() {
-            isEnabled = true
-        }
-
-        guard let nightscoutTab = tabBarController?.tabBar.items![3] else { return }
-        nightscoutTab.isEnabled = isEnabled
-    }
+    // MARK: Life-cycle
 
     override func viewDidLoad() {
         super.viewDidLoad()
-        if Storage.shared.forceDarkMode.value {
-            overrideUserInterfaceStyle = .dark
-        }
-
-        let buildDetails = BuildDetails.default
-        let formattedBuildDate = dateTimeUtils.formattedDate(from: buildDetails.buildDate())
-        let branchAndSha = buildDetails.branchAndSha
-        let expiration = dateTimeUtils.formattedDate(from: buildDetails.calculateExpirationDate())
-        let expirationHeaderString = buildDetails.expirationHeaderString
-        let versionManager = AppVersionManager()
-        let version = versionManager.version()
-        let isMacApp = buildDetails.isMacApp()
-        let isSimulatorBuild = buildDetails.isSimulatorBuild()
-
-        form
-            +++ Section(header: "Data Settings", footer: "")
-            <<< SegmentedRow<String>("units") { row in
-                row.title = "Units"
-                row.options = ["mg/dL", "mmol/L"]
-                row.value = UserDefaultsRepository.units.value
-            }.onChange { row in
-                guard let value = row.value else { return }
-                UserDefaultsRepository.units.value = value
-            }
-            <<< ButtonRow("nightscout") {
-                $0.title = "Nightscout Settings"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentNightscoutSettingsView()
-                        return UIViewController()
-                    }), onDismiss: nil
-                )
-            }
-            <<< ButtonRow("dexcom") {
-                $0.title = "Dexcom Settings"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentDexcomSettingsView()
-                        return UIViewController()
-                    }), onDismiss: nil
-                )
-            }
-
-            +++ Section("App Settings")
-
-            <<< ButtonRow("backgroundRefreshSettings") {
-                $0.title = "Background Refresh Settings"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentBackgroundRefreshSettings()
-                        return UIViewController()
-                    }),
-                    onDismiss: nil
-                )
-            }
-
-            <<< ButtonRow {
-                $0.title = "General Settings"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentGeneralSettings()
-                        return UIViewController()
-                    }),
-                    onDismiss: nil
-                )
-            }
-            <<< ButtonRow("graphSettings") {
-                $0.title = "Graph Settings"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentGraphSettings()
-                        return UIViewController()
-                    }),
-                    onDismiss: nil
-                )
-            }
-            <<< ButtonRow("informationDisplaySettings") {
-                $0.title = "Information Display Settings"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentInfoDisplaySettings()
-                        return UIViewController()
-                    }
-                    ), onDismiss: nil
-                )
-            }
-
-            <<< ButtonRow("alarmsList") {
-                $0.title = "Alarms"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentAlarmList()
-                        return UIViewController()
-                    }),
-                    onDismiss: nil
-                )
-            }
-
-            <<< ButtonRow("alarmsSettings") {
-                $0.title = "Alarm Settings"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentAlarmSettings()
-                        return UIViewController()
-                    }),
-                    onDismiss: nil
-                )
-            }
-
-            <<< ButtonRow("remoteSettings") {
-                $0.title = "Remote Settings"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentRemoteSettings()
-                        return UIViewController()
-                    }),
-                    onDismiss: nil
-                )
-            }
-
-            +++ Section("Integrations")
-            <<< ButtonRow {
-                $0.title = "Calendar"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        let host = UIHostingController(rootView: WatchSettingsView())
-                        host.modalPresentationStyle = .formSheet
-                        if Storage.shared.forceDarkMode.value {
-                            host.overrideUserInterfaceStyle = .dark
-                        }
-                        return host
-                    }),
-                    onDismiss: nil
-                )
-            }
-            <<< ButtonRow("contact") {
-                $0.title = "Contact"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentContactSettings()
-                        return UIViewController()
-                    }
-                    ), onDismiss: nil
-                )
-            }
-            +++ Section("Advanced Settings")
-            <<< ButtonRow {
-                $0.title = "Advanced Settings"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentAdvancedSettingsView()
-                        return UIViewController()
-                    }), onDismiss: nil
-                )
-            }
-
-            +++ Section("Logging")
-            <<< ButtonRow("viewlog") {
-                $0.title = "View Log"
-                $0.presentationMode = .show(
-                    controllerProvider: .callback(builder: {
-                        self.presentLogView()
-                        return UIViewController()
-                    }), onDismiss: nil
-                )
-            }
-            <<< ButtonRow("shareLogs") {
-                $0.title = "Share Logs"
-                $0.cellSetup { cell, _ in
-                    cell.accessibilityIdentifier = "ShareLogsButton"
-                }
-                $0.onCellSelection { [weak self] _, _ in
-                    self?.shareLogs()
-                }
-            }
-
-            +++ Section("Build Information")
-            <<< LabelRow {
-                $0.title = "Version"
-                $0.value = version
-                $0.tag = "currentVersionRow"
-            }
-            <<< LabelRow {
-                $0.title = "Latest version"
-                $0.value = "Fetching..."
-                $0.tag = "latestVersionRow"
-            }
-            <<< LabelRow {
-                $0.title = expirationHeaderString
-                $0.value = expiration
-                $0.hidden = Condition(booleanLiteral: isMacApp || isSimulatorBuild)
-            }
-            <<< LabelRow {
-                $0.title = "Built"
-                $0.value = formattedBuildDate
-            }
-            <<< LabelRow {
-                $0.title = "Branch"
-                $0.value = branchAndSha
-            }
-
-        showHideNSDetails()
-    }
-
-    override func viewWillAppear(_ animated: Bool) {
-        super.viewWillAppear(animated)
-
-        refreshVersionInfo()
-    }
-
-    func refreshVersionInfo() {
-        let versionManager = AppVersionManager()
-        versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in
-            DispatchQueue.main.async {
-                if let currentVersionRow = self.form.rowBy(tag: "currentVersionRow") as? LabelRow {
-                    currentVersionRow.cell.detailTextLabel?.textColor = self.getColor(isBlacklisted: isBlacklisted, isNewer: isNewer, isCurrent: latestVersion == versionManager.version())
-                    currentVersionRow.updateCell()
-                }
-
-                if let latestVersionRow = self.form.rowBy(tag: "latestVersionRow") as? LabelRow {
-                    latestVersionRow.value = latestVersion ?? "Unknown"
-                    latestVersionRow.updateCell()
-                }
-            }
-        }
-    }
-
-    private func getColor(isBlacklisted: Bool, isNewer: Bool, isCurrent: Bool) -> UIColor {
-        if isBlacklisted {
-            return .red
-        } else if isNewer {
-            return .orange
-        } else if isCurrent {
-            return .green
-        } else {
-            return .secondaryLabel
-        }
-    }
-
-    func presentInfoDisplaySettings() {
-        let viewModel = InfoDisplaySettingsViewModel()
-        let settingsView = InfoDisplaySettingsView(viewModel: viewModel)
-
-        let hostingController = UIHostingController(rootView: settingsView)
-        hostingController.modalPresentationStyle = .formSheet
-
-        if Storage.shared.forceDarkMode.value {
-            hostingController.overrideUserInterfaceStyle = .dark
-        }
-
-        present(hostingController, animated: true, completion: nil)
-    }
-
-    func presentRemoteSettings() {
-        let viewModel = RemoteSettingsViewModel()
-        let settingsView = RemoteSettingsView(viewModel: viewModel)
-        let hostingController = UIHostingController(rootView: settingsView)
-        hostingController.modalPresentationStyle = .formSheet
-
-        if Storage.shared.forceDarkMode.value {
-            hostingController.overrideUserInterfaceStyle = .dark
-        }
-
-        present(hostingController, animated: true, completion: nil)
-    }
-
-    func presentAlarmSettings() {
-        let settingsView = AlarmSettingsView()
-        let hostingController = UIHostingController(rootView: settingsView)
-        hostingController.modalPresentationStyle = .formSheet
-
-        if Storage.shared.forceDarkMode.value {
-            hostingController.overrideUserInterfaceStyle = .dark
-        }
-
-        present(hostingController, animated: true, completion: nil)
-    }
-
-    func presentAlarmList() {
-        let settingsView = AlarmListView()
-        let hostingController = UIHostingController(rootView: settingsView)
-        hostingController.modalPresentationStyle = .formSheet
-
-        if Storage.shared.forceDarkMode.value {
-            hostingController.overrideUserInterfaceStyle = .dark
-        }
-
-        present(hostingController, animated: true, completion: nil)
-    }
-
-    func presentContactSettings() {
-        let viewModel = ContactSettingsViewModel()
-        let contactSettingsView = ContactSettingsView(viewModel: viewModel)
-        let hostingController = UIHostingController(rootView: contactSettingsView)
-        hostingController.modalPresentationStyle = .formSheet
-
-        if Storage.shared.forceDarkMode.value {
-            hostingController.overrideUserInterfaceStyle = .dark
-        }
-
-        present(hostingController, animated: true, completion: nil)
-    }
-
-    func presentBackgroundRefreshSettings() {
-        let viewModel = BackgroundRefreshSettingsViewModel()
-        let settingsView = BackgroundRefreshSettingsView(viewModel: viewModel)
-        let hostingController = UIHostingController(rootView: settingsView)
-        hostingController.modalPresentationStyle = .formSheet
-
-        if Storage.shared.forceDarkMode.value {
-            hostingController.overrideUserInterfaceStyle = .dark
-        }
-
-        present(hostingController, animated: true, completion: nil)
-    }
-
-    func presentLogView() {
-        let viewModel = LogViewModel()
-        let logView = LogView(viewModel: viewModel)
-        let hostingController = UIHostingController(rootView: logView)
-        hostingController.modalPresentationStyle = .formSheet
-
-        if Storage.shared.forceDarkMode.value {
-            hostingController.overrideUserInterfaceStyle = .dark
-        }
-
-        present(hostingController, animated: true, completion: nil)
-    }
-
-    func presentNightscoutSettingsView() {
-        let viewModel = NightscoutSettingsViewModel()
-        viewModel.delegate = self
-
-        let view = NightscoutSettingsView(viewModel: viewModel)
-        let hostingController = UIHostingController(rootView: view)
-        hostingController.modalPresentationStyle = .formSheet
-
-        if Storage.shared.forceDarkMode.value {
-            hostingController.overrideUserInterfaceStyle = .dark
-        }
-
-        present(hostingController, animated: true, completion: nil)
-    }
-
-    func nightscoutSettingsDidFinish() {
-        showHideNSDetails()
-    }
-
-    func presentDexcomSettingsView() {
-        let viewModel = DexcomSettingsViewModel()
-        let settingsView = DexcomSettingsView(viewModel: viewModel)
-        let hostingController = UIHostingController(rootView: settingsView)
-        hostingController.modalPresentationStyle = .formSheet
-
-        if Storage.shared.forceDarkMode.value {
-            hostingController.overrideUserInterfaceStyle = .dark
-        }
-
-        present(hostingController, animated: true, completion: nil)
-    }
-
-    func presentAdvancedSettingsView() {
-        let viewModel = AdvancedSettingsViewModel()
-        let view = AdvancedSettingsView(viewModel: viewModel)
-        let hostingController = UIHostingController(rootView: view)
-        hostingController.modalPresentationStyle = .formSheet
-
-        if Storage.shared.forceDarkMode.value {
-            hostingController.overrideUserInterfaceStyle = .dark
-        }
-
-        present(hostingController, animated: true, completion: nil)
-    }
 
-    func presentGeneralSettings() {
-        let view = GeneralSettingsView()
-        let hostingController = UIHostingController(rootView: view)
-        hostingController.modalPresentationStyle = .formSheet
-
-        if Storage.shared.forceDarkMode.value {
-            hostingController.overrideUserInterfaceStyle = .dark
-        }
-        present(hostingController, animated: true)
-    }
-
-    func presentGraphSettings() {
-        let view = GraphSettingsView()
-        let host = UIHostingController(rootView: view)
-        host.modalPresentationStyle = .formSheet
+        // Build SwiftUI menu
+        host = UIHostingController(
+            rootView: SettingsMenuView { [weak self] nightscoutEnabled in
+                self?.tabBarController?.tabBar.items?[3].isEnabled = nightscoutEnabled
+            })
 
+        // Dark-mode override
         if Storage.shared.forceDarkMode.value {
             host.overrideUserInterfaceStyle = .dark
         }
 
-        present(host, animated: true)
-    }
-
-    private func shareLogs() {
-        let logFilesToShare = LogManager.shared.logFilesForTodayAndYesterday()
-
-        if !logFilesToShare.isEmpty {
-            let activityViewController = UIActivityViewController(activityItems: logFilesToShare, applicationActivities: nil)
-            activityViewController.popoverPresentationController?.sourceView = view
-            present(activityViewController, animated: true, completion: nil)
-        } else {
-            let alert = UIAlertController(title: "No Logs Available", message: "There are no logs to share.", preferredStyle: .alert)
-            alert.addAction(UIAlertAction(title: "OK", style: .default))
-            present(alert, animated: true, completion: nil)
-        }
+        // Embed
+        addChild(host)
+        host.view.translatesAutoresizingMaskIntoConstraints = false
+        view.addSubview(host.view)
+        NSLayoutConstraint.activate([
+            host.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            host.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            host.view.topAnchor.constraint(equalTo: view.topAnchor),
+            host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+        ])
+        host.didMove(toParent: self)
     }
 }