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)