From f852b81cdf13850e39f94de93623f99f14fd8ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 6 Nov 2026 22:26:30 +0100 Subject: [PATCH] Show the display name on the watch With Show Display Name on, the phone sends the app's display name in the watch state. The watch's glucose page shows it at the top left, and the rectangular, corner and inline complications include it. Default LoopFollow_ names truncate at the head so the suffix stays visible. --- LoopFollow/Watch/WatchSessionManager.swift | 1 + LoopFollow/Watch/WatchStateBuilder.swift | 5 +- LoopFollowWatch/GlucosePage.swift | 19 ++++++++ LoopFollowWatch/StandaloneFetcher.swift | 3 +- LoopFollowWatch/WatchSession.swift | 2 +- .../GlucoseComplicationView.swift | 47 +++++++++++++------ Tests/WatchProtocolTests.swift | 13 ++++- WatchShared/WatchState.swift | 2 + 8 files changed, 73 insertions(+), 19 deletions(-) diff --git a/LoopFollow/Watch/WatchSessionManager.swift b/LoopFollow/Watch/WatchSessionManager.swift index 041169524..5f1f29563 100644 --- a/LoopFollow/Watch/WatchSessionManager.swift +++ b/LoopFollow/Watch/WatchSessionManager.swift @@ -55,6 +55,7 @@ final class WatchSessionManager: NSObject { Storage.shared.watchRemoteCommands.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.remoteType.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.colorBGText.$value.map { _ in () }.eraseToAnyPublisher(), + Storage.shared.showDisplayName.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.units.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.mealWithBolus.$value.map { _ in () }.eraseToAnyPublisher(), Storage.shared.mealWithFatProtein.$value.map { _ in () }.eraseToAnyPublisher(), diff --git a/LoopFollow/Watch/WatchStateBuilder.swift b/LoopFollow/Watch/WatchStateBuilder.swift index 12a379c99..3c50bcd2b 100644 --- a/LoopFollow/Watch/WatchStateBuilder.swift +++ b/LoopFollow/Watch/WatchStateBuilder.swift @@ -67,7 +67,10 @@ enum WatchStateBuilder { stats: vc.map { stats($0.statsDisplayModel) }, device: Storage.shared.device.value, alarm: currentAlarm(), - remote: remoteConfig() + remote: remoteConfig(), + displayName: Storage.shared.showDisplayName.value + ? Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String + : nil ) } diff --git a/LoopFollowWatch/GlucosePage.swift b/LoopFollowWatch/GlucosePage.swift index d07086dd2..aeaf7a22d 100644 --- a/LoopFollowWatch/GlucosePage.swift +++ b/LoopFollowWatch/GlucosePage.swift @@ -27,6 +27,11 @@ struct GlucosePage: View { } } .toolbar { + if let name = state.displayName { + ToolbarItem(placement: .topBarLeading) { + DisplayNameLabel(name: name) + } + } ToolbarItem(placement: .topBarTrailing) { age(now: context.date) } @@ -261,3 +266,17 @@ struct AlarmBanner: View { } } } + +/// The app's display name. Names built from the default `LoopFollow_` pattern +/// truncate at the head so the suffix stays visible. +struct DisplayNameLabel: View { + let name: String + + var body: some View { + Text(name) + .font(.footnote.weight(.semibold)) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(name.hasPrefix("LoopFollow") ? .head : .tail) + } +} diff --git a/LoopFollowWatch/StandaloneFetcher.swift b/LoopFollowWatch/StandaloneFetcher.swift index 0617371c0..96703b643 100644 --- a/LoopFollowWatch/StandaloneFetcher.swift +++ b/LoopFollowWatch/StandaloneFetcher.swift @@ -150,7 +150,8 @@ enum StandaloneFetcher { stats: base?.stats, device: base?.device ?? "", alarm: nil, - remote: remote + remote: remote, + displayName: base?.displayName ) } diff --git a/LoopFollowWatch/WatchSession.swift b/LoopFollowWatch/WatchSession.swift index 1c7d97eac..71af54ba4 100644 --- a/LoopFollowWatch/WatchSession.swift +++ b/LoopFollowWatch/WatchSession.swift @@ -267,7 +267,7 @@ final class WatchSession: NSObject, ObservableObject { private static func complicationKey(_ state: WatchState) -> String { let s = state.snapshot - return "\(s?.updatedAt.timeIntervalSince1970 ?? 0)|\(s?.unit.rawValue ?? "")|\(state.lowMgdl)|\(state.highMgdl)|\(state.colorBGText)" + return "\(s?.updatedAt.timeIntervalSince1970 ?? 0)|\(s?.unit.rawValue ?? "")|\(state.lowMgdl)|\(state.highMgdl)|\(state.colorBGText)|\(state.displayName ?? "")" } private func receiveCredentials(_ data: Data) { diff --git a/LoopFollowWatchWidgets/GlucoseComplicationView.swift b/LoopFollowWatchWidgets/GlucoseComplicationView.swift index 7e42e5f29..b002e4536 100644 --- a/LoopFollowWatchWidgets/GlucoseComplicationView.swift +++ b/LoopFollowWatchWidgets/GlucoseComplicationView.swift @@ -39,6 +39,15 @@ struct GlucoseComplicationView: View { snapshot.map { WatchFormat.minutesAgo($0.updatedAt, now: entry.date) } ?? "" } + private var displayName: String? { entry.state?.displayName } + + /// Names built from the default `LoopFollow_` pattern truncate at the head so the suffix stays visible. + private func nameText(_ name: String) -> some View { + Text(name) + .lineLimit(1) + .truncationMode(name.hasPrefix("LoopFollow") ? .head : .tail) + } + var body: some View { switch family { case .accessoryCircular: @@ -71,21 +80,28 @@ struct GlucoseComplicationView: View { private var rectangular: some View { HStack(spacing: 6) { VStack(alignment: .leading, spacing: 0) { - HStack(alignment: .firstTextBaseline, spacing: 2) { - Text(bgText) - .font(.system(size: 26, weight: .bold)) - .foregroundStyle(bgColor) - .strikethrough(isStale, color: .red) - Text(trendText) - .font(.system(size: 14, weight: .bold)) + if let displayName { + nameText(displayName) + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(.secondary) + } + VStack(alignment: .leading, spacing: 0) { + HStack(alignment: .firstTextBaseline, spacing: 2) { + Text(bgText) + .font(.system(size: 26, weight: .bold)) + .foregroundStyle(bgColor) + .strikethrough(isStale, color: .red) + Text(trendText) + .font(.system(size: 14, weight: .bold)) + } + Text("\(deltaText) \(ageText)") + .font(.system(size: 12)) + .foregroundStyle(.secondary) } - Text("\(deltaText) \(ageText)") - .font(.system(size: 12)) - .foregroundStyle(.secondary) + .lineLimit(1) + .minimumScaleFactor(0.6) + .widgetAccentable() } - .lineLimit(1) - .minimumScaleFactor(0.6) - .widgetAccentable() sparkline } } @@ -122,11 +138,12 @@ struct GlucoseComplicationView: View { .foregroundStyle(bgColor) .widgetCurvesContent() .widgetLabel { - Text("\(trendText) \(deltaText) \(ageText)") + Text([displayName, trendText, deltaText, ageText].compactMap { $0 }.joined(separator: " ")) } } private var inline: some View { - Text(snapshot == nil ? "LoopFollow --" : "\(bgText) \(trendText) \(deltaText) · \(ageText)") + let reading = snapshot == nil ? "--" : "\(bgText) \(trendText) \(deltaText) · \(ageText)" + return Text(snapshot == nil && displayName == nil ? "LoopFollow --" : [displayName, reading].compactMap { $0 }.joined(separator: " ")) } } diff --git a/Tests/WatchProtocolTests.swift b/Tests/WatchProtocolTests.swift index ebb5231b0..cf01f54f3 100644 --- a/Tests/WatchProtocolTests.swift +++ b/Tests/WatchProtocolTests.swift @@ -45,7 +45,8 @@ struct WatchProtocolTests { tempTargetPresets: [], quickPickBoluses: [1, 2.5], quickPickMeals: [WatchMealPick(carbs: 45, fat: 0, protein: 0, bolus: 0)] - ) + ), + displayName: "Emma" ) } @@ -56,6 +57,16 @@ struct WatchProtocolTests { #expect(WatchState.decode(data) == original) } + @Test("A state without a display name decodes with none") + func stateWithoutDisplayName() throws { + let data = try #require(state().encoded()) + var json = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any]) + json["displayName"] = nil + let decoded = try #require(WatchState.decode(JSONSerialization.data(withJSONObject: json))) + #expect(decoded.displayName == nil) + #expect(decoded.snapshot == state().snapshot) + } + @Test("The active override is the one whose period contains the time") func activeOverride() { let state = state() diff --git a/WatchShared/WatchState.swift b/WatchShared/WatchState.swift index 91c73c69c..f0cebb801 100644 --- a/WatchShared/WatchState.swift +++ b/WatchShared/WatchState.swift @@ -41,6 +41,8 @@ struct WatchState: Codable, Equatable { var alarm: WatchAlarm? /// Present when remote commands from the watch are enabled on the phone. var remote: WatchRemoteConfig? + /// The app's display name, present while the phone's "Show Display Name" setting is on. + var displayName: String? func encoded() -> Data? { try? JSONEncoder().encode(self)