Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion BuildTools/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ let package = Package(
dependencies: [
.package(
url: "https://github.com/nicklockwood/SwiftFormat.git",
from: "0.56.1"
.exact("0.56.1")
),
],
targets: [
Expand Down
6 changes: 4 additions & 2 deletions LoopFollow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
656F8C102E49F36F0008DC1D /* QRCodeDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */; };
656F8C122E49F3780008DC1D /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */; };
656F8C142E49F3D20008DC1D /* RemoteCommandSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */; };
657F98182F043D8100F732BD /* HomeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657F98172F043D8100F732BD /* HomeContentView.swift */; };
6584B1012E4A263900135D4D /* TOTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6584B1002E4A263900135D4D /* TOTPService.swift */; };
6589CC622E9E7D1600BB18FE /* ImportExportSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC532E9E7D1600BB18FE /* ImportExportSettingsView.swift */; };
6589CC632E9E7D1600BB18FE /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC5D2E9E7D1600BB18FE /* GeneralSettingsView.swift */; };
Expand All @@ -34,7 +35,6 @@
6589CC712E9E814F00BB18FE /* AlarmSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */; };
6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */; };
65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; };
65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; };
65E8A2862E44B0300065037B /* VolumeButtonHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */; };
DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; };
DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; };
Expand Down Expand Up @@ -416,6 +416,7 @@
656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeDisplayView.swift; sourceTree = "<group>"; };
656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = "<group>"; };
656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCommandSettings.swift; sourceTree = "<group>"; };
657F98172F043D8100F732BD /* HomeContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeContentView.swift; sourceTree = "<group>"; };
6584B1002E4A263900135D4D /* TOTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPService.swift; sourceTree = "<group>"; };
6589CC522E9E7D1600BB18FE /* ExportableSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportableSettings.swift; sourceTree = "<group>"; };
6589CC532E9E7D1600BB18FE /* ImportExportSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportExportSettingsView.swift; sourceTree = "<group>"; };
Expand All @@ -434,7 +435,6 @@
6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSelectionView.swift; sourceTree = "<group>"; };
6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMigrationManager.swift; sourceTree = "<group>"; };
65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = "<group>"; };
65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = "<group>"; };
65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtonHandler.swift; sourceTree = "<group>"; };
A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -854,6 +854,7 @@
6589CC5C2E9E7D1600BB18FE /* DexcomSettingsViewModel.swift */,
6589CC5D2E9E7D1600BB18FE /* GeneralSettingsView.swift */,
6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */,
657F98172F043D8100F732BD /* HomeContentView.swift */,
6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */,
6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */,
);
Expand Down Expand Up @@ -2053,6 +2054,7 @@
6589CC6C2E9E7D1600BB18FE /* GraphSettingsView.swift in Sources */,
6589CC6D2E9E7D1600BB18FE /* CalendarSettingsView.swift in Sources */,
6589CC6E2E9E7D1600BB18FE /* SettingsMenuView.swift in Sources */,
657F98182F043D8100F732BD /* HomeContentView.swift in Sources */,
6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */,
DD493ADF2ACF22BB009A6922 /* SAge.swift in Sources */,
DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion LoopFollow/Application/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<!--Home-->
<scene sceneID="hNz-n2-bh7">
<objects>
<viewController id="9pv-A4-QxB" userLabel="Home" customClass="MainViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="MainViewController" id="9pv-A4-QxB" userLabel="Home" customClass="MainViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="tsR-hK-woN">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
Expand Down
14 changes: 7 additions & 7 deletions LoopFollow/Controllers/MainViewController+updateStats.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ extension MainViewController {

let stats = StatsData(bgData: lastDayOfData)

statsLowPercent.text = String(format: "%.1f%", stats.percentLow) + "%"
statsInRangePercent.text = String(format: "%.1f%", stats.percentRange) + "%"
statsHighPercent.text = String(format: "%.1f%", stats.percentHigh) + "%"
statsAvgBG.text = Localizer.toDisplayUnits(String(format: "%.0f%", stats.avgBG))
statsLowPercent.text = String(format: "%.1f%%", stats.percentLow)
statsInRangePercent.text = String(format: "%.1f%%", stats.percentRange)
statsHighPercent.text = String(format: "%.1f%%", stats.percentHigh)
statsAvgBG.text = Localizer.toDisplayUnits(String(format: "%.0f", stats.avgBG))
if Storage.shared.useIFCC.value {
statsEstA1C.text = String(format: "%.0f%", stats.a1C)
statsEstA1C.text = String(format: "%.0f", stats.a1C)
} else {
statsEstA1C.text = String(format: "%.1f%", stats.a1C)
statsEstA1C.text = String(format: "%.1f", stats.a1C)
}
statsStdDev.text = String(format: "%.2f%", stats.stdDev)
statsStdDev.text = String(format: "%.2f", stats.stdDev)

createStatsPie(pieData: stats.pie)
}
Expand Down
78 changes: 75 additions & 3 deletions LoopFollow/Helpers/TabPosition.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,90 @@
// LoopFollow
// TabPosition.swift

enum TabPosition: String, CaseIterable, Codable {
enum TabPosition: String, CaseIterable, Codable, Comparable {
case position1
case position2
case position3
case position4
case menu
case more
case disabled

var displayName: String {
switch self {
case .position1: return "Tab 1"
case .position2: return "Tab 2"
case .position3: return "Tab 3"
case .position4: return "Tab 4"
case .more: return "More Menu"
case .disabled: return "Hidden"
case .menu, .more, .disabled: return "Menu"
}
}

/// The index in the tab bar (0-based)
var tabIndex: Int? {
switch self {
case .position1: return 0
case .position2: return 1
case .position3: return 2
case .position4: return 3
case .menu, .more, .disabled: return 4
}
}

/// Positions that users can customize (1-4)
static var customizablePositions: [TabPosition] {
[.position1, .position2, .position3, .position4]
}

/// Normalize legacy values to current values
var normalized: TabPosition {
switch self {
case .more, .disabled: return .menu
default: return self
}
}

// Comparable conformance for sorting
static func < (lhs: TabPosition, rhs: TabPosition) -> Bool {
let order: [TabPosition] = [.position1, .position2, .position3, .position4, .menu, .more, .disabled]
guard let lhsIndex = order.firstIndex(of: lhs),
let rhsIndex = order.firstIndex(of: rhs) else { return false }
return lhsIndex < rhsIndex
}
}

/// Represents a tab item that can be placed in any position
enum TabItem: String, CaseIterable, Codable, Identifiable {
case home
case alarms
case remote
case nightscout
case snoozer

var id: String { rawValue }

var displayName: String {
switch self {
case .home: return "Home"
case .alarms: return "Alarms"
case .remote: return "Remote"
case .nightscout: return "Nightscout"
case .snoozer: return "Snoozer"
}
}

var icon: String {
switch self {
case .home: return "house"
case .alarms: return "alarm"
case .remote: return "antenna.radiowaves.left.and.right"
case .nightscout: return "safari"
case .snoozer: return "zzz"
}
}

/// Items that can be moved between tab bar and menu (all except settings which doesn't exist as a tab)
static var movableItems: [TabItem] {
[.home, .alarms, .remote, .nightscout, .snoozer]
}
}
80 changes: 80 additions & 0 deletions LoopFollow/Settings/HomeContentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// LoopFollow
// HomeContentView.swift

import SwiftUI
import UIKit

/// A SwiftUI wrapper around MainViewController that displays the full Home screen.
/// This can be used both in the tab bar and as a modal from the Menu.
struct HomeContentView: UIViewControllerRepresentable {
let isModal: Bool

init(isModal: Bool = false) {
self.isModal = isModal
}

func makeUIViewController(context _: Context) -> UIViewController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)

// Get the MainViewController from storyboard
guard let mainVC = storyboard.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController else {
let fallbackVC = UIViewController()
fallbackVC.view.backgroundColor = .systemBackground
let label = UILabel()
label.text = "Unable to load Home screen"
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
fallbackVC.view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: fallbackVC.view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: fallbackVC.view.centerYAnchor),
])
return fallbackVC
}

if Storage.shared.forceDarkMode.value {
mainVC.overrideUserInterfaceStyle = .dark
}

mainVC.isPresentedAsModal = isModal

return mainVC
}

func updateUIViewController(_ uiViewController: UIViewController, context _: Context) {
if Storage.shared.forceDarkMode.value {
uiViewController.overrideUserInterfaceStyle = .dark
} else {
uiViewController.overrideUserInterfaceStyle = .unspecified
}
}
}

// MARK: - Modal wrapper with navigation bar

struct HomeModalView: View {
@Environment(\.dismiss) private var dismiss

var body: some View {
NavigationView {
HomeContentView(isModal: true)
.navigationTitle("Home")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") {
dismiss()
}
.fontWeight(.semibold)
}
}
}
.preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil)
}
}

// MARK: - Preview

#Preview {
HomeModalView()
}
51 changes: 5 additions & 46 deletions LoopFollow/Settings/SettingsMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,7 @@ struct SettingsMenuView: View {
}

// ───────── Alarms ─────────
Section {
NavigationRow(title: "Alarms",
icon: "bell")
{
settingsPath.value.append(Sheet.alarmsList)
}

Section("Alarms") {
NavigationRow(title: "Alarm Settings",
icon: "bell.badge")
{
Expand Down Expand Up @@ -126,13 +120,6 @@ struct SettingsMenuView: View {
action: shareLogs)
}

// ───────── Community ─────────
Section("Community") {
LinkRow(title: "LoopFollow Facebook Group",
icon: "person.2.fill",
url: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!)
}

// ───────── Build info ─────────
buildInfoSection
}
Expand All @@ -142,8 +129,7 @@ struct SettingsMenuView: View {
TabCustomizationModal(
isPresented: $showingTabCustomization,
onApply: {
// Dismiss any presented view controller and go to home tab
handleTabReorganization()
// No-op - changes are applied silently via observers
}
)
}
Expand Down Expand Up @@ -239,34 +225,8 @@ struct SettingsMenuView: View {
}

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
}
// Rebuild the tab bar with the new configuration
MainViewController.rebuildTabsIfNeeded()
}
}

Expand All @@ -277,7 +237,7 @@ private enum Sheet: Hashable, Identifiable {
case backgroundRefresh
case general, graph
case infoDisplay
case alarmsList, alarmSettings
case alarmSettings
case remote
case importExport
case calendar, contact
Expand All @@ -295,7 +255,6 @@ private enum Sheet: Hashable, Identifiable {
case .general: GeneralSettingsView()
case .graph: GraphSettingsView()
case .infoDisplay: InfoDisplaySettingsView(viewModel: .init())
case .alarmsList: AlarmListView()
case .alarmSettings: AlarmSettingsView()
case .remote: RemoteSettingsView(viewModel: .init())
case .importExport: ImportExportSettingsView()
Expand Down
Loading