Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7a30989
Enhance tab customization system.
codebymini Jan 2, 2026
84381e3
Add statistics view
codebymini Nov 22, 2025
7f5ace0
Add treatment list
codebymini Nov 22, 2025
fd6c2b2
Fix pagination for treatment list
codebymini Nov 23, 2025
6267d2c
Replace "Force Dark Mode" with dynamic Appearance setting (System/Lig…
imbercal Jan 20, 2026
d642b28
Automatic lints from Xcode
codebymini Jan 29, 2026
aeb8848
Fix appearanceMode
codebymini Jan 29, 2026
e2c3d1e
Revert "Automatic lints from Xcode"
codebymini Jan 29, 2026
d7026e5
Fix for bolus calculations
codebymini Jan 30, 2026
45a0a90
Resolve darkmode issues after rebase
codebymini Jan 31, 2026
573912f
Add filters for treatments
codebymini Jan 31, 2026
4241611
Add treatments and stats to new tab handler
codebymini Jan 31, 2026
a0e99e3
Sort treatments on date
codebymini Feb 7, 2026
1cf2183
Add filters for bolus and switch labels between Loop/Trio
codebymini Feb 7, 2026
540a9cc
Use date picker for statistics page and update loading ui
codebymini Feb 7, 2026
9212493
Make up til last full day selectable for statistics view
codebymini Feb 8, 2026
a8b13aa
Persist state for GMI/Std Dev and TIT/TITR toggles in AggregatedStats…
codebymini Feb 28, 2026
9e40512
Fix line breaks in stats
codebymini Feb 28, 2026
21e0358
Enhance TIRView to display cutoff values with units in the legend
codebymini Feb 28, 2026
949e851
Refactor insulin statistics display and calculations in AggregatedSta…
codebymini Feb 28, 2026
7dba3a4
Add BG formatting functions and enhance treatment types for overrides…
codebymini Feb 28, 2026
76f353b
Merge branch 'dev' into new-stats
codebymini Feb 28, 2026
9a03f9d
Clean up unused code
codebymini Mar 1, 2026
788305f
Add missing } at end of file
codebymini Mar 2, 2026
f3551a2
Use checkmark buttons to close menu items
codebymini Mar 2, 2026
74e7bfc
Load data for stats when Main view is in menu
codebymini Mar 2, 2026
9ef7d76
Add confirm button for tab settings
codebymini Mar 2, 2026
2681b3a
Merge remote-tracking branch 'upstream/dev' into new-stats
codebymini Mar 2, 2026
4cb09f3
Update treatments to adhere to time zone settings
codebymini Mar 2, 2026
f3f9df8
Move timezone setting into General settings
codebymini Mar 2, 2026
c626bfc
Enhance TreatmentsView with time zone support and UI adjustments
codebymini Mar 2, 2026
f1ebc0b
Add time zone support to info table in MainViewController
codebymini Mar 2, 2026
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
14 changes: 14 additions & 0 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 */; };
657F99E92F0BC81500F732BD /* OTPSecureMessenger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657F99E82F0BC81500F732BD /* OTPSecureMessenger.swift */; };
6584B1012E4A263900135D4D /* TOTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6584B1002E4A263900135D4D /* TOTPService.swift */; };
6589CC622E9E7D1600BB18FE /* ImportExportSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC532E9E7D1600BB18FE /* ImportExportSettingsView.swift */; };
Expand All @@ -34,6 +35,7 @@
6589CC6F2E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC572E9E7D1600BB18FE /* AdvancedSettingsViewModel.swift */; };
6589CC712E9E814F00BB18FE /* AlarmSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC702E9E814F00BB18FE /* AlarmSelectionView.swift */; };
6589CC752E9EAFB700BB18FE /* SettingsMigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589CC742E9EAFB700BB18FE /* SettingsMigrationManager.swift */; };
6584B1012E4A263900135D4D /* TOTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6584B1002E4A263900135D4D /* TOTPService.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 */; };
Expand Down Expand Up @@ -419,6 +421,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>"; };
657F99E82F0BC81500F732BD /* OTPSecureMessenger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPSecureMessenger.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>"; };
Expand All @@ -437,6 +440,7 @@
6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationModal.swift; sourceTree = "<group>"; };
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>"; };
6584B1002E4A263900135D4D /* TOTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPService.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; };
Expand Down Expand Up @@ -810,6 +814,8 @@
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedRootGroup section */
65AC25F52ECFD5E800421360 /* Stats */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Stats; sourceTree = "<group>"; };
65AC26702ED245DF00421360 /* Treatments */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Treatments; sourceTree = "<group>"; };
DDCC3AD72DDE1790006F1C10 /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */

Expand Down Expand Up @@ -860,6 +866,7 @@
6589CC5C2E9E7D1600BB18FE /* DexcomSettingsViewModel.swift */,
6589CC5D2E9E7D1600BB18FE /* GeneralSettingsView.swift */,
6589CC5E2E9E7D1600BB18FE /* GraphSettingsView.swift */,
657F98172F043D8100F732BD /* HomeContentView.swift */,
6589CC5F2E9E7D1600BB18FE /* SettingsMenuView.swift */,
6589CC602E9E7D1600BB18FE /* TabCustomizationModal.swift */,
);
Expand Down Expand Up @@ -1477,6 +1484,8 @@
isa = PBXGroup;
children = (
6589CC612E9E7D1600BB18FE /* Settings */,
65AC26702ED245DF00421360 /* Treatments */,
65AC25F52ECFD5E800421360 /* Stats */,
DDCF9A7E2D85FCE6004DF4DD /* Alarm */,
FC16A97624995FEE003D6245 /* Application */,
DDFF3D792D140F1800BF9D9E /* BackgroundRefresh */,
Expand Down Expand Up @@ -1633,6 +1642,10 @@
);
dependencies = (
);
fileSystemSynchronizedGroups = (
65AC25F52ECFD5E800421360 /* Stats */,
65AC26702ED245DF00421360 /* Treatments */,
);
name = LoopFollow;
packageProductDependencies = (
DD48781B2C7DAF140048F05C /* SwiftJWT */,
Expand Down Expand Up @@ -2064,6 +2077,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
96 changes: 0 additions & 96 deletions LoopFollow.xcworkspace/xcshareddata/swiftpm/Package.resolved

This file was deleted.

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
4 changes: 3 additions & 1 deletion LoopFollow/Controllers/Nightscout/Treatments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ extension MainViewController {
if !Storage.shared.downloadTreatments.value { return }

let startTimeString = dateTimeUtils.getDateTimeString(addingDays: -1 * Storage.shared.downloadDays.value)
let currentTimeString = dateTimeUtils.getDateTimeString(addingHours: 6)
let currentTimeString = dateTimeUtils.getDateTimeString()
let estimatedCount = max(Storage.shared.downloadDays.value * 100, 5000)
let parameters: [String: String] = [
"find[created_at][$gte]": startTimeString,
"find[created_at][$lte]": currentTimeString,
"count": "\(estimatedCount)",
]
NightscoutUtils.executeDynamicRequest(eventType: .treatments, parameters: parameters) { (result: Result<Any, Error>) in
switch result {
Expand Down
19 changes: 19 additions & 0 deletions LoopFollow/Helpers/DateTime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@
import Foundation

class dateTimeUtils {
static func displayTimeZone() -> TimeZone {
if Storage.shared.graphTimeZoneEnabled.value,
let tz = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value)
{
return tz
}
return .current
}

static func displayCalendar() -> Calendar {
var calendar = Calendar.current
calendar.timeZone = displayTimeZone()
return calendar
}

static func applyDisplayTimeZone(to formatter: DateFormatter) {
formatter.timeZone = displayTimeZone()
}

static func getTimeIntervalMidnightToday() -> TimeInterval {
let now = Date()
let formatter = DateFormatter()
Expand Down
84 changes: 81 additions & 3 deletions LoopFollow/Helpers/TabPosition.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,96 @@
// 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
case treatments
case stats

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"
case .treatments: return "Treatments"
case .stats: return "Statistics"
}
}

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"
case .treatments: return "cross.case"
case .stats: return "chart.bar.xaxis"
}
}

/// Items that can be moved between tab bar and menu (all except settings which doesn't exist as a tab)
static var movableItems: [TabItem] {
[.home, .alarms, .remote, .nightscout, .snoozer, .treatments, .stats]
}
}
31 changes: 31 additions & 0 deletions LoopFollow/Settings/GeneralSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct GeneralSettingsView: View {
@ObservedObject var snoozerEmoji = Storage.shared.snoozerEmoji
@ObservedObject var forcePortraitMode = Storage.shared.forcePortraitMode
@ObservedObject var persistentNotification = Storage.shared.persistentNotification
@ObservedObject var graphTimeZoneEnabled = Storage.shared.graphTimeZoneEnabled
@ObservedObject var graphTimeZoneIdentifier = Storage.shared.graphTimeZoneIdentifier

// Speak-BG settings
@ObservedObject var speakBG = Storage.shared.speakBG
Expand Down Expand Up @@ -61,6 +63,20 @@ struct GeneralSettingsView: View {
}
}

Section("Time Zone") {
Toggle("Time Zone Override", isOn: $graphTimeZoneEnabled.value)
.onChange(of: graphTimeZoneEnabled.value) { _ in markChartSettingsDirty() }

if graphTimeZoneEnabled.value {
Picker("Time Zone", selection: $graphTimeZoneIdentifier.value) {
ForEach(Self.sortedTimeZones, id: \.identifier) { tz in
Text(Self.timeZoneLabel(tz)).tag(tz.identifier)
}
}
.onChange(of: graphTimeZoneIdentifier.value) { _ in markChartSettingsDirty() }
}
}

Section("Speak BG") {
Toggle("Speak BG", isOn: $speakBG.value.animation())

Expand Down Expand Up @@ -122,4 +138,19 @@ struct GeneralSettingsView: View {
.preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
.navigationBarTitle("General Settings", displayMode: .inline)
}

private func markChartSettingsDirty() {
Observable.shared.chartSettingsChanged.value = true
}

private static let sortedTimeZones: [TimeZone] = TimeZone.knownTimeZoneIdentifiers
.compactMap { TimeZone(identifier: $0) }
.sorted { $0.secondsFromGMT() < $1.secondsFromGMT() }

private static func timeZoneLabel(_ tz: TimeZone) -> String {
let offsetMinutes = tz.secondsFromGMT() / 60
let sign = offsetMinutes >= 0 ? "+" : "-"
let offsetString = String(format: "UTC%@%02d:%02d", sign, abs(offsetMinutes) / 60, abs(offsetMinutes) % 60)
return "(\(offsetString)) \(tz.identifier)"
}
}
Loading