Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
54630a7
Add an Apple Watch app fed by the phone, with remote commands for Loo…
bjorkert Oct 19, 2026
f761eec
Put the watch Remote page before Info, allow watch commands by defaul…
bjorkert Oct 19, 2026
56fa351
Make the watch app glanceable: one large BG, actions in the bottom to…
bjorkert Oct 19, 2026
eb38244
Point browser builders to the docs when the watch identifiers are not…
bjorkert Oct 19, 2026
89420cc
Keep watch Info values clear of the page indicator and widen the Meal…
bjorkert Oct 19, 2026
96961e4
Check that the watch identifiers exist before match, since its read-o…
bjorkert Oct 19, 2026
b27b854
Mirror the phone's info table on the watch: same rows, order and colo…
bjorkert Oct 19, 2026
e3bc6b2
Share the APNs, JWT, TRC encryption and Loop payload code between the…
bjorkert Oct 19, 2026
e15f77b
Send credentials to the watch when they change, and share command val…
bjorkert Oct 19, 2026
70bdf2e
Let the watch fetch from Nightscout or Dexcom and send commands direc…
bjorkert Oct 19, 2026
4a36ce2
Show the phone's statistics on the watch and mark data fetched by the…
bjorkert Oct 19, 2026
93709a1
Scrub the watch chart with the Digital Crown and pick the time span f…
bjorkert Oct 19, 2026
488db47
Test watch command validation, signing key choice, devicestatus parsi…
bjorkert Oct 19, 2026
93f31dc
Schedule the watch background refresh once the app is running
bjorkert Oct 19, 2026
58ca297
Deliver credentials to the watch by message as well as by transfer, a…
bjorkert Oct 19, 2026
d7b3ad6
Convert Nightscout dates on the watch and give up waiting for the pho…
bjorkert Oct 19, 2026
0f6a56c
Leave out watch chart hour labels too close to the edges to fit
bjorkert Oct 19, 2026
c7908d9
Show only the reading age on the watch glucose page, keep the toolbar…
bjorkert Oct 20, 2026
119dd14
Let the crown scroll the watch meal screen until a field is tapped
bjorkert Oct 21, 2026
6a84c2f
Show Other Amount on the watch bolus list as plain text
bjorkert Oct 21, 2026
ce6aab8
Fit the watch statistics page on one screen on every watch size and w…
bjorkert Oct 21, 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
356 changes: 356 additions & 0 deletions LoopFollow.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions LoopFollow/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

UNUserNotificationCenter.current().delegate = self

WatchSessionManager.shared.activate()

// Only spin up Bluetooth if the user has chosen a BLE-based background
// refresh. Initializing BLEManager creates a CBCentralManager, which
// triggers the Bluetooth permission prompt — deferring it keeps that
Expand Down
1 change: 1 addition & 0 deletions LoopFollow/Charts/BGChartModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ final class BGChartModel: ObservableObject {
guard let self else { return }
self.rebuildScheduled = false
self.performRebuild()
WatchSessionManager.shared.requestUpdate(reason: "chart")
}
}

Expand Down
20 changes: 15 additions & 5 deletions LoopFollow/InfoTable/InfoDisplayItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,25 @@ struct InfoColoring: Codable, Equatable {
/// the default text color); when enabled, an in-range value reads green,
/// mirroring the app's BG text coloring.
func color(for value: Double, direction: InfoColorDirection) -> Color? {
switch level(for: value, direction: direction) {
case .urgent: .red
case .warning: .yellow
case .normal: .green
case nil: nil
}
}

/// The threshold band a value falls in, or `nil` when coloring is off.
func level(for value: Double, direction: InfoColorDirection) -> InfoColorLevel? {
guard enabled else { return nil }
switch direction {
case .above:
if let urgent, value >= urgent { return .red }
if let warning, value >= warning { return .yellow }
if let urgent, value >= urgent { return .urgent }
if let warning, value >= warning { return .warning }
case .below:
if let urgent, value <= urgent { return .red }
if let warning, value <= warning { return .yellow }
if let urgent, value <= urgent { return .urgent }
if let warning, value <= warning { return .warning }
}
return .green
return .normal
}
}
37 changes: 1 addition & 36 deletions LoopFollow/LiveActivity/GlucoseSnapshotBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ enum GlucoseSnapshotBuilder {

let preferredUnit = PreferredGlucoseUnit.snapshotUnit()
let deltaMgdl = provider.deltaMgdl ?? 0.0
let trend = mapTrend(provider.trendCode)
let trend = GlucoseSnapshot.Trend(code: provider.trendCode)

if provider.showRenewalOverlay {
LogManager.shared.log(category: .general, message: "[LA] renewal overlay ON")
Expand Down Expand Up @@ -180,39 +180,4 @@ enum GlucoseSnapshotBuilder {
showRenewalOverlay: provider.showRenewalOverlay,
)
}

// MARK: - Trend Mapping

private static func mapTrend(_ code: String?) -> GlucoseSnapshot.Trend {
guard
let raw = code?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(),
!raw.isEmpty
else { return .unknown }

if raw.contains("doubleup") || raw.contains("rapidrise") || raw == "up2" || raw == "upfast" {
return .upFast
}
if raw.contains("fortyfiveup") {
return .upSlight
}
if raw.contains("singleup") || raw == "up" || raw == "up1" || raw == "rising" {
return .up
}
if raw.contains("flat") || raw == "steady" || raw == "none" {
return .flat
}
if raw.contains("doubledown") || raw.contains("rapidfall") || raw == "down2" || raw == "downfast" {
return .downFast
}
if raw.contains("fortyfivedown") {
return .downSlight
}
if raw.contains("singledown") || raw == "down" || raw == "down1" || raw == "falling" {
return .down
}

return .unknown
}
}
4 changes: 2 additions & 2 deletions LoopFollow/LiveActivity/LiveActivityManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ final class LiveActivityManager {
DispatchQueue.main.async { self.refreshFromCurrentState(reason: reason) }
return
}
WatchSessionManager.shared.requestUpdate(reason: reason)
refreshCoalescer.request(reason: reason)
}

Expand Down Expand Up @@ -1064,10 +1065,9 @@ final class LiveActivityManager {
let snapshotUnchanged = GlucoseSnapshotStore.shared.load() == snapshot
let thresholdsChanged = LAAppGroupSettings.thresholdsMgdl() != UnitSettingsStore.shared.effectiveThresholds()

// Store + Watch: always update, independent of LA state.
// Store: always update, independent of LA state.
writeThresholds()
GlucoseSnapshotStore.shared.save(snapshot)
// WatchConnectivityManager.shared.send(snapshot: snapshot)

// LA update: gated on LA being active, snapshot having changed, and activities enabled.
if !Storage.shared.laEnabled.value {
Expand Down
26 changes: 0 additions & 26 deletions LoopFollow/Remote/LoopAPNS/LoopAPNSCarbsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -623,29 +623,3 @@ struct LoopAPNSCarbsView: View {
min(max(date, oldestAcceptedDate), latestAcceptedDate)
}
}

// APNS Payload structure for carbs
struct LoopAPNSPayload {
enum PayloadType {
case carbs
case bolus
}

let type: PayloadType
let carbsAmount: Double?
let absorptionTime: Double?
let foodType: String?
let bolusAmount: Double?
let consumedDate: Date?
let otp: String

init(type: PayloadType, carbsAmount: Double? = nil, absorptionTime: Double? = nil, foodType: String? = nil, bolusAmount: Double? = nil, consumedDate: Date? = nil, otp: String) {
self.type = type
self.carbsAmount = carbsAmount
self.absorptionTime = absorptionTime
self.foodType = foodType
self.bolusAmount = bolusAmount
self.consumedDate = consumedDate
self.otp = otp
}
}
Loading