From f8aaa6f3b07daa5c4431df3a8cbec9be316aa740 Mon Sep 17 00:00:00 2001 From: Sebastian Engel Date: Sat, 19 Sep 2026 11:49:45 +0200 Subject: [PATCH] refactor(settings): remove trend arrow toggle and trend code The trend arrow no longer exists in the UI, so drop the "Show trend arrow" setting on iOS and watchOS along with everything behind it: - TrendArrowPreferences, SettingsKey.trendArrowEnabled, WeightTrendArrow view - WeightTrendEvaluator, WeightTrendDirection, WeightTrendCache and tests - unused localization strings in all 11 locales (Show trend arrow, Display, Trending up, Trending down, Weight stable) Entry widget sync now fetches only the latest weight (limit 1). Co-Authored-By: Claude Sonnet 5 --- .../Resources/de.lproj/Localizable.strings | 5 - .../Resources/en.lproj/Localizable.strings | 5 - .../Resources/es.lproj/Localizable.strings | 5 - .../Resources/fr.lproj/Localizable.strings | 5 - .../Resources/it.lproj/Localizable.strings | 5 - .../Resources/ja.lproj/Localizable.strings | 5 - .../Resources/ko.lproj/Localizable.strings | 5 - .../Resources/nl.lproj/Localizable.strings | 5 - .../Resources/pt-BR.lproj/Localizable.strings | 5 - .../zh-Hans.lproj/Localizable.strings | 5 - .../zh-Hant.lproj/Localizable.strings | 5 - App/Shared/Views/SettingsView.swift | 10 -- App/Shared/Widget/WeightTrendArrow.swift | 61 -------- App/Shared/Widget/WidgetTimelineRefresh.swift | 3 +- App/Watch/Views/WatchSettingsView.swift | 9 -- .../Models/WeightTrendEvaluator.swift | 122 --------------- .../LogWeightCore/Settings/SettingsKeys.swift | 1 - .../Settings/SettingsMigrator.swift | 1 - .../Settings/TrendArrowPreferences.swift | 34 ---- .../Widget/WeightTrendCache.swift | 64 -------- .../TrendArrowPreferencesTests.swift | 37 ----- .../WeightTrendCacheTests.swift | 29 ---- .../WeightTrendEvaluatorTests.swift | 146 ------------------ project.yml | 1 - 24 files changed, 1 insertion(+), 572 deletions(-) delete mode 100644 App/Shared/Widget/WeightTrendArrow.swift delete mode 100644 Packages/LogWeightCore/Sources/LogWeightCore/Models/WeightTrendEvaluator.swift delete mode 100644 Packages/LogWeightCore/Sources/LogWeightCore/Settings/TrendArrowPreferences.swift delete mode 100644 Packages/LogWeightCore/Sources/LogWeightCore/Widget/WeightTrendCache.swift delete mode 100644 Packages/LogWeightCore/Tests/LogWeightCoreTests/TrendArrowPreferencesTests.swift delete mode 100644 Packages/LogWeightCore/Tests/LogWeightCoreTests/WeightTrendCacheTests.swift delete mode 100644 Packages/LogWeightCore/Tests/LogWeightCoreTests/WeightTrendEvaluatorTests.swift diff --git a/App/Shared/Resources/de.lproj/Localizable.strings b/App/Shared/Resources/de.lproj/Localizable.strings index b37acca..58fe471 100644 --- a/App/Shared/Resources/de.lproj/Localizable.strings +++ b/App/Shared/Resources/de.lproj/Localizable.strings @@ -65,11 +65,6 @@ "Open Settings" = "Einstellungen öffnen"; "Log your weight" = "Gewicht eintragen"; "Open LogWeight to record today's weight." = "Öffne LogWeight, um dein heutiges Gewicht zu erfassen."; -"Display" = "Anzeige"; -"Show trend arrow" = "Trendpfeil anzeigen"; -"Trending up" = "Steigender Trend"; -"Trending down" = "Fallender Trend"; -"Weight stable" = "Gewicht stabil"; "Cannot Delete Entry" = "Eintrag kann nicht gelöscht werden"; "OK" = "OK"; "Delete" = "Löschen"; diff --git a/App/Shared/Resources/en.lproj/Localizable.strings b/App/Shared/Resources/en.lproj/Localizable.strings index 7ff21fc..fdf7619 100644 --- a/App/Shared/Resources/en.lproj/Localizable.strings +++ b/App/Shared/Resources/en.lproj/Localizable.strings @@ -65,11 +65,6 @@ "Open Settings" = "Open Settings"; "Log your weight" = "Log your weight"; "Open LogWeight to record today's weight." = "Open LogWeight to record today's weight."; -"Display" = "Display"; -"Show trend arrow" = "Show trend arrow"; -"Trending up" = "Trending up"; -"Trending down" = "Trending down"; -"Weight stable" = "Weight stable"; "Cannot Delete Entry" = "Cannot Delete Entry"; "OK" = "OK"; "Delete" = "Delete"; diff --git a/App/Shared/Resources/es.lproj/Localizable.strings b/App/Shared/Resources/es.lproj/Localizable.strings index 3dc39f2..7144d2e 100644 --- a/App/Shared/Resources/es.lproj/Localizable.strings +++ b/App/Shared/Resources/es.lproj/Localizable.strings @@ -65,11 +65,6 @@ "Open Settings" = "Abrir Ajustes"; "Log your weight" = "Registra tu peso"; "Open LogWeight to record today's weight." = "Abre LogWeight para registrar el peso de hoy."; -"Display" = "Visualización"; -"Show trend arrow" = "Mostrar flecha de tendencia"; -"Trending up" = "Tendencia al alza"; -"Trending down" = "Tendencia a la baja"; -"Weight stable" = "Peso estable"; "Cannot Delete Entry" = "No se puede eliminar el registro"; "OK" = "OK"; "Delete" = "Eliminar"; diff --git a/App/Shared/Resources/fr.lproj/Localizable.strings b/App/Shared/Resources/fr.lproj/Localizable.strings index 156a861..3dab994 100644 --- a/App/Shared/Resources/fr.lproj/Localizable.strings +++ b/App/Shared/Resources/fr.lproj/Localizable.strings @@ -65,11 +65,6 @@ "Open Settings" = "Ouvrir Réglages"; "Log your weight" = "Enregistrez votre poids"; "Open LogWeight to record today's weight." = "Ouvrez LogWeight pour enregistrer le poids du jour."; -"Display" = "Affichage"; -"Show trend arrow" = "Afficher la flèche de tendance"; -"Trending up" = "Tendance à la hausse"; -"Trending down" = "Tendance à la baisse"; -"Weight stable" = "Poids stable"; "Cannot Delete Entry" = "Impossible de supprimer l’entrée"; "OK" = "OK"; "Delete" = "Supprimer"; diff --git a/App/Shared/Resources/it.lproj/Localizable.strings b/App/Shared/Resources/it.lproj/Localizable.strings index efd48bd..c5ff374 100644 --- a/App/Shared/Resources/it.lproj/Localizable.strings +++ b/App/Shared/Resources/it.lproj/Localizable.strings @@ -65,11 +65,6 @@ "Open Settings" = "Apri Impostazioni"; "Log your weight" = "Registra il peso"; "Open LogWeight to record today's weight." = "Apri LogWeight per registrare il peso di oggi."; -"Display" = "Visualizzazione"; -"Show trend arrow" = "Mostra freccia di tendenza"; -"Trending up" = "In aumento"; -"Trending down" = "In diminuzione"; -"Weight stable" = "Peso stabile"; "Cannot Delete Entry" = "Impossibile eliminare la voce"; "OK" = "OK"; "Delete" = "Elimina"; diff --git a/App/Shared/Resources/ja.lproj/Localizable.strings b/App/Shared/Resources/ja.lproj/Localizable.strings index 04d2191..5d97188 100644 --- a/App/Shared/Resources/ja.lproj/Localizable.strings +++ b/App/Shared/Resources/ja.lproj/Localizable.strings @@ -65,11 +65,6 @@ "Open Settings" = "設定を開く"; "Log your weight" = "体重を記録しましょう"; "Open LogWeight to record today's weight." = "LogWeightを開いて、今日の体重を記録してください。"; -"Display" = "表示"; -"Show trend arrow" = "トレンド矢印を表示"; -"Trending up" = "上昇傾向"; -"Trending down" = "下降傾向"; -"Weight stable" = "体重は安定"; "Cannot Delete Entry" = "記録を削除できません"; "OK" = "OK"; "Delete" = "削除"; diff --git a/App/Shared/Resources/ko.lproj/Localizable.strings b/App/Shared/Resources/ko.lproj/Localizable.strings index 5b8a274..5413d57 100644 --- a/App/Shared/Resources/ko.lproj/Localizable.strings +++ b/App/Shared/Resources/ko.lproj/Localizable.strings @@ -65,11 +65,6 @@ "Open Settings" = "설정 열기"; "Log your weight" = "체중을 기록하세요"; "Open LogWeight to record today's weight." = "LogWeight를 열어 오늘의 체중을 기록하세요."; -"Display" = "표시"; -"Show trend arrow" = "추세 화살표 표시"; -"Trending up" = "상승 추세"; -"Trending down" = "하락 추세"; -"Weight stable" = "체중 안정"; "Cannot Delete Entry" = "기록을 삭제할 수 없음"; "OK" = "확인"; "Delete" = "삭제"; diff --git a/App/Shared/Resources/nl.lproj/Localizable.strings b/App/Shared/Resources/nl.lproj/Localizable.strings index 0735a21..936c5e7 100644 --- a/App/Shared/Resources/nl.lproj/Localizable.strings +++ b/App/Shared/Resources/nl.lproj/Localizable.strings @@ -65,11 +65,6 @@ "Open Settings" = "Open Instellingen"; "Log your weight" = "Log je gewicht"; "Open LogWeight to record today's weight." = "Open LogWeight om het gewicht van vandaag vast te leggen."; -"Display" = "Weergave"; -"Show trend arrow" = "Trendpijl tonen"; -"Trending up" = "Stijgende trend"; -"Trending down" = "Dalende trend"; -"Weight stable" = "Gewicht stabiel"; "Cannot Delete Entry" = "Invoer kan niet worden verwijderd"; "OK" = "OK"; "Delete" = "Verwijderen"; diff --git a/App/Shared/Resources/pt-BR.lproj/Localizable.strings b/App/Shared/Resources/pt-BR.lproj/Localizable.strings index 9319abf..abfd8d3 100644 --- a/App/Shared/Resources/pt-BR.lproj/Localizable.strings +++ b/App/Shared/Resources/pt-BR.lproj/Localizable.strings @@ -65,11 +65,6 @@ "Open Settings" = "Abrir Ajustes"; "Log your weight" = "Registre seu peso"; "Open LogWeight to record today's weight." = "Abra o LogWeight para registrar o peso de hoje."; -"Display" = "Exibição"; -"Show trend arrow" = "Mostrar seta de tendência"; -"Trending up" = "Em alta"; -"Trending down" = "Em queda"; -"Weight stable" = "Peso estável"; "Cannot Delete Entry" = "Não é possível excluir o registro"; "OK" = "OK"; "Delete" = "Excluir"; diff --git a/App/Shared/Resources/zh-Hans.lproj/Localizable.strings b/App/Shared/Resources/zh-Hans.lproj/Localizable.strings index 6a27d6a..7f1615f 100644 --- a/App/Shared/Resources/zh-Hans.lproj/Localizable.strings +++ b/App/Shared/Resources/zh-Hans.lproj/Localizable.strings @@ -65,11 +65,6 @@ "Open Settings" = "打开设置"; "Log your weight" = "记录体重"; "Open LogWeight to record today's weight." = "打开 LogWeight 记录今日体重。"; -"Display" = "显示"; -"Show trend arrow" = "显示趋势箭头"; -"Trending up" = "上升趋势"; -"Trending down" = "下降趋势"; -"Weight stable" = "体重稳定"; "Cannot Delete Entry" = "无法删除记录"; "OK" = "好"; "Delete" = "删除"; diff --git a/App/Shared/Resources/zh-Hant.lproj/Localizable.strings b/App/Shared/Resources/zh-Hant.lproj/Localizable.strings index c95f4c4..e25dda3 100644 --- a/App/Shared/Resources/zh-Hant.lproj/Localizable.strings +++ b/App/Shared/Resources/zh-Hant.lproj/Localizable.strings @@ -65,11 +65,6 @@ "Open Settings" = "打開設定"; "Log your weight" = "記錄體重"; "Open LogWeight to record today's weight." = "打開 LogWeight 記錄今日體重。"; -"Display" = "顯示"; -"Show trend arrow" = "顯示趨勢箭頭"; -"Trending up" = "上升趨勢"; -"Trending down" = "下降趨勢"; -"Weight stable" = "體重穩定"; "Cannot Delete Entry" = "無法刪除記錄"; "OK" = "好"; "Delete" = "刪除"; diff --git a/App/Shared/Views/SettingsView.swift b/App/Shared/Views/SettingsView.swift index d7e99d7..9d2fa1a 100644 --- a/App/Shared/Views/SettingsView.swift +++ b/App/Shared/Views/SettingsView.swift @@ -9,7 +9,6 @@ struct SettingsView: View { @AppStorage(SettingsKey.unitPreference) private var unitPreferenceRaw: String = WeightUnit.kilograms.rawValue @AppStorage(SettingsKey.defaultEntryMode) private var defaultEntryModeRaw: String = DefaultEntryMode.lastSaved.rawValue @AppStorage(SettingsKey.hapticsEnabled) private var hapticsEnabled: Bool = true - @AppStorage(SettingsKey.trendArrowEnabled) private var trendArrowEnabled: Bool = true @AppStorage(SettingsKey.reminderEnabled) private var reminderEnabled: Bool = false @AppStorage(SettingsKey.reminderHour) private var reminderHour: Int = DailyReminderSettings.defaultHour @AppStorage(SettingsKey.reminderMinute) private var reminderMinute: Int = DailyReminderSettings.defaultMinute @@ -48,11 +47,6 @@ struct SettingsView: View { .accessibilityIdentifier("settings.haptics") } - Section("Display") { - Toggle("Show trend arrow", isOn: $trendArrowEnabled) - .accessibilityIdentifier("settings.trendArrow") - } - #if os(iOS) Section("Reminders") { Toggle("Daily reminder", isOn: reminderEnabledBinding) @@ -126,10 +120,6 @@ struct SettingsView: View { WeightDisplayPreferences.mirrorUnitPreferenceToAppGroup() WidgetTimelineRefresh.reloadEntryAndChartWidgets() } - .onChange(of: trendArrowEnabled) { _, _ in - TrendArrowPreferences.mirrorToAppGroup() - WidgetTimelineRefresh.reloadEntryAndChartWidgets() - } #if os(iOS) .task { reminderAuthStatus = await reminderScheduler.authorizationStatus() diff --git a/App/Shared/Widget/WeightTrendArrow.swift b/App/Shared/Widget/WeightTrendArrow.swift deleted file mode 100644 index 9fe7bdf..0000000 --- a/App/Shared/Widget/WeightTrendArrow.swift +++ /dev/null @@ -1,61 +0,0 @@ -import SwiftUI -import LogWeightCore - -private enum TrendArrowPreferenceStore { - nonisolated(unsafe) static let userDefaults = - UserDefaults(suiteName: SharedWeightEntryStore.appGroupIdentifier) ?? .standard -} - -/// Compact trend indicator for widgets and history headers. -struct WeightTrendArrow: View { - let direction: WeightTrendDirection - var font: Font = .caption2.weight(.semibold) - - @AppStorage(SettingsKey.trendArrowEnabled, store: TrendArrowPreferenceStore.userDefaults) - private var trendArrowEnabled = true - - /// Shared style for Home Screen widget headers (gray, subheadline size). - static func widget(direction: WeightTrendDirection) -> WeightTrendArrow { - WeightTrendArrow(direction: direction, font: .subheadline.weight(.semibold)) - } - - var body: some View { - if trendArrowEnabled { - trendContent - } else { - EmptyView() - } - } - - @ViewBuilder - private var trendContent: some View { - switch direction { - case .unknown: - EmptyView() - case .up: - trendSymbol("arrow.up", accessibilityLabel: "Trending up") - case .down: - trendSymbol("arrow.down", accessibilityLabel: "Trending down") - case .flat: - trendSymbol("minus", accessibilityLabel: "Weight stable") - } - } - - private func trendSymbol(_ name: String, accessibilityLabel: String) -> some View { - Image(systemName: name) - .font(font) - .foregroundStyle(.secondary) - .accessibilityLabel(accessibilityLabel) - } -} - -#if DEBUG -#Preview("Trend arrows") { - HStack(spacing: 12) { - WeightTrendArrow(direction: .up) - WeightTrendArrow(direction: .down) - WeightTrendArrow(direction: .flat) - } - .padding() -} -#endif diff --git a/App/Shared/Widget/WidgetTimelineRefresh.swift b/App/Shared/Widget/WidgetTimelineRefresh.swift index 224baa5..cf12a45 100644 --- a/App/Shared/Widget/WidgetTimelineRefresh.swift +++ b/App/Shared/Widget/WidgetTimelineRefresh.swift @@ -24,9 +24,8 @@ enum WidgetTimelineRefresh { static func syncEntryStoreAndReloadWidgets(store: HealthKitStore) async { #if os(iOS) - let weights = (try? await store.recentWeights(limit: WeightTrendCache.entryWidgetSampleLimit)) ?? [] + let weights = (try? await store.recentWeights(limit: 1)) ?? [] SharedWeightEntryStore.syncFromLatestWeight(weights.first) - WeightTrendCache.update(from: weights) reloadEntryAndChartWidgets() #endif } diff --git a/App/Watch/Views/WatchSettingsView.swift b/App/Watch/Views/WatchSettingsView.swift index 35f9ba1..9d60c30 100644 --- a/App/Watch/Views/WatchSettingsView.swift +++ b/App/Watch/Views/WatchSettingsView.swift @@ -1,5 +1,4 @@ import SwiftUI -import WidgetKit import LogWeightCore /// Minimal watch settings (unit + haptics). Full settings remain on iPhone. @@ -7,7 +6,6 @@ struct WatchSettingsView: View { @AppStorage(SettingsKey.unitPreference) private var unitPreferenceRaw: String = WeightUnit.kilograms.rawValue @AppStorage(SettingsKey.hapticsEnabled) private var hapticsEnabled: Bool = true - @AppStorage(SettingsKey.trendArrowEnabled) private var trendArrowEnabled: Bool = true @Environment(\.dismiss) private var dismiss var body: some View { @@ -24,9 +22,6 @@ struct WatchSettingsView: View { Section("Feedback") { Toggle("Haptics on save", isOn: $hapticsEnabled) } - Section("Display") { - Toggle("Show trend arrow", isOn: $trendArrowEnabled) - } Section("About") { Text("Made with 🩷🩵 by Juuronina GbR.") .font(.footnote) @@ -41,10 +36,6 @@ struct WatchSettingsView: View { Button("Done") { dismiss() } } } - .onChange(of: trendArrowEnabled) { _, _ in - TrendArrowPreferences.mirrorToAppGroup() - WidgetCenter.shared.reloadTimelines(ofKind: LogWeightWidgetConstants.watchKind) - } } } } diff --git a/Packages/LogWeightCore/Sources/LogWeightCore/Models/WeightTrendEvaluator.swift b/Packages/LogWeightCore/Sources/LogWeightCore/Models/WeightTrendEvaluator.swift deleted file mode 100644 index 30a209a..0000000 --- a/Packages/LogWeightCore/Sources/LogWeightCore/Models/WeightTrendEvaluator.swift +++ /dev/null @@ -1,122 +0,0 @@ -import Foundation - -/// Direction of recent body-weight change for compact UI (widgets, complications). -public enum WeightTrendDirection: Sendable, Equatable { - case up - case down - case flat - case unknown -} - -/// Tunables for `WeightTrendEvaluator.direction`. -public struct WeightTrendConfiguration: Sendable, Equatable { - public var minimumSampleCount: Int - public var minimumSpanDays: Double - /// Slopes with magnitude below this (kg/day) are treated as flat. - public var flatSlopeThresholdKgPerDay: Double - - public init( - minimumSampleCount: Int = 3, - minimumSpanDays: Double = 7, - flatSlopeThresholdKgPerDay: Double = 0.02 - ) { - self.minimumSampleCount = minimumSampleCount - self.minimumSpanDays = minimumSpanDays - self.flatSlopeThresholdKgPerDay = flatSlopeThresholdKgPerDay - } - - /// Relaxed gates for short chart windows (e.g. 1W widget) so a visible trend can be shown. - public static func forChartRange(_ range: ChartTimeRange) -> WeightTrendConfiguration { - switch range { - case .oneWeek: - WeightTrendConfiguration( - minimumSampleCount: 2, - minimumSpanDays: 1, - flatSlopeThresholdKgPerDay: 0.015 - ) - case .oneMonth: - WeightTrendConfiguration( - minimumSampleCount: 3, - minimumSpanDays: 4, - flatSlopeThresholdKgPerDay: 0.02 - ) - case .threeMonths, .sixMonths: - WeightTrendConfiguration( - minimumSampleCount: 3, - minimumSpanDays: 7, - flatSlopeThresholdKgPerDay: 0.02 - ) - case .oneYear, .all: - WeightTrendConfiguration() - } - } -} - -/// Estimates whether weight is trending up, down, or flat using OLS slope over a time window. -public enum WeightTrendEvaluator { - private static let secondsPerDay: Double = 86_400 - - public static func direction( - weights: [Weight], - windowStart: Date?, - referenceDate: Date = .now, - configuration: WeightTrendConfiguration = WeightTrendConfiguration() - ) -> WeightTrendDirection { - let inWindow = weights.filter { weight in - weight.recordedAt <= referenceDate - && (windowStart.map { weight.recordedAt >= $0 } ?? true) - } - let sorted = inWindow.sorted { $0.recordedAt < $1.recordedAt } - - guard sorted.count >= configuration.minimumSampleCount else { - return .unknown - } - - guard let first = sorted.first, let last = sorted.last else { - return .unknown - } - - let spanDays = last.recordedAt.timeIntervalSince(first.recordedAt) / Self.secondsPerDay - guard spanDays >= configuration.minimumSpanDays else { - return .unknown - } - - guard let slopeKgPerDay = linearSlopeKgPerDay(samples: sorted, origin: first.recordedAt) else { - return .unknown - } - - let threshold = configuration.flatSlopeThresholdKgPerDay - if slopeKgPerDay > threshold { - return .up - } - if slopeKgPerDay < -threshold { - return .down - } - return .flat - } - - /// Ordinary least-squares slope (kg/day) with x = days since `origin`. - private static func linearSlopeKgPerDay(samples: [Weight], origin: Date) -> Double? { - guard samples.count >= 2 else { return nil } - - var sumX = 0.0 - var sumY = 0.0 - var sumXX = 0.0 - var sumXY = 0.0 - let n = Double(samples.count) - - for sample in samples { - let x = sample.recordedAt.timeIntervalSince(origin) / secondsPerDay - let y = sample.valueInKilograms - sumX += x - sumY += y - sumXX += x * x - sumXY += x * y - } - - let denominator = n * sumXX - sumX * sumX - guard denominator > 0 else { return nil } - - return (n * sumXY - sumX * sumY) / denominator - } -} diff --git a/Packages/LogWeightCore/Sources/LogWeightCore/Settings/SettingsKeys.swift b/Packages/LogWeightCore/Sources/LogWeightCore/Settings/SettingsKeys.swift index 6c94abc..2b39512 100644 --- a/Packages/LogWeightCore/Sources/LogWeightCore/Settings/SettingsKeys.swift +++ b/Packages/LogWeightCore/Sources/LogWeightCore/Settings/SettingsKeys.swift @@ -12,7 +12,6 @@ public enum SettingsKey { public static let unitPreference = "logweight_unit_preference" public static let defaultEntryMode = "logweight_default_entry_mode" public static let hapticsEnabled = "logweight_haptics_enabled" - public static let trendArrowEnabled = "logweight_trend_arrow_enabled" public static let reminderEnabled = "logweight_reminder_enabled" public static let reminderHour = "logweight_reminder_hour" public static let reminderMinute = "logweight_reminder_minute" diff --git a/Packages/LogWeightCore/Sources/LogWeightCore/Settings/SettingsMigrator.swift b/Packages/LogWeightCore/Sources/LogWeightCore/Settings/SettingsMigrator.swift index 4840b81..950a988 100644 --- a/Packages/LogWeightCore/Sources/LogWeightCore/Settings/SettingsMigrator.swift +++ b/Packages/LogWeightCore/Sources/LogWeightCore/Settings/SettingsMigrator.swift @@ -23,6 +23,5 @@ public enum SettingsMigrator { private static func mirrorWidgetPreferences(standardDefaults: UserDefaults) { WeightDisplayPreferences.mirrorUnitPreferenceToAppGroup(standardDefaults: standardDefaults) - TrendArrowPreferences.mirrorToAppGroup(standardDefaults: standardDefaults) } } diff --git a/Packages/LogWeightCore/Sources/LogWeightCore/Settings/TrendArrowPreferences.swift b/Packages/LogWeightCore/Sources/LogWeightCore/Settings/TrendArrowPreferences.swift deleted file mode 100644 index a20967e..0000000 --- a/Packages/LogWeightCore/Sources/LogWeightCore/Settings/TrendArrowPreferences.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation - -/// Whether trend arrows appear in widgets, complications, and the History chart header. -public enum TrendArrowPreferences { - /// Reads app-group defaults first (widgets), then standard defaults (app / watch). Defaults to enabled. - public static func isEnabled( - sharedDefaults: UserDefaults? = UserDefaults(suiteName: SharedWeightEntryStore.appGroupIdentifier), - standardDefaults: UserDefaults = .standard - ) -> Bool { - for defaults in [sharedDefaults, standardDefaults].compactMap({ $0 }) { - if let enabled = bool(from: defaults) { - return enabled - } - } - return true - } - - /// Copies the app preference into the App Group so widget extensions can read it. - public static func mirrorToAppGroup( - standardDefaults: UserDefaults = .standard, - sharedDefaults: UserDefaults? = UserDefaults(suiteName: SharedWeightEntryStore.appGroupIdentifier) - ) { - guard let shared = sharedDefaults else { return } - let enabled = standardDefaults.object(forKey: SettingsKey.trendArrowEnabled) as? Bool ?? true - shared.set(enabled, forKey: SettingsKey.trendArrowEnabled) - } - - private static func bool(from defaults: UserDefaults) -> Bool? { - guard defaults.object(forKey: SettingsKey.trendArrowEnabled) != nil else { - return nil - } - return defaults.bool(forKey: SettingsKey.trendArrowEnabled) - } -} diff --git a/Packages/LogWeightCore/Sources/LogWeightCore/Widget/WeightTrendCache.swift b/Packages/LogWeightCore/Sources/LogWeightCore/Widget/WeightTrendCache.swift deleted file mode 100644 index 697a293..0000000 --- a/Packages/LogWeightCore/Sources/LogWeightCore/Widget/WeightTrendCache.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation - -/// Cached 28-day weight trend for widget extensions (read synchronously from the App Group). -public enum WeightTrendCache { - private static let directionKey = "logweight_cached_trend_direction" - /// Matches the entry widget's HealthKit window. - public static let entryWidgetLookbackDays = 28 - public static let entryWidgetSampleLimit = 40 - - public static func save( - _ direction: WeightTrendDirection, - userDefaults: UserDefaults? = UserDefaults(suiteName: SharedWeightEntryStore.appGroupIdentifier) - ) { - guard let defaults = userDefaults else { return } - defaults.set(direction.storedValue, forKey: directionKey) - } - - public static func load( - userDefaults: UserDefaults? = UserDefaults(suiteName: SharedWeightEntryStore.appGroupIdentifier) - ) -> WeightTrendDirection { - guard - let defaults = userDefaults, - let raw = defaults.string(forKey: directionKey) - else { - return .unknown - } - return WeightTrendDirection(storedValue: raw) - } - - public static func update( - from weights: [Weight], - referenceDate: Date = .now, - calendar: Calendar = .current, - userDefaults: UserDefaults? = UserDefaults(suiteName: SharedWeightEntryStore.appGroupIdentifier) - ) { - let windowStart = calendar.date(byAdding: .day, value: -entryWidgetLookbackDays, to: referenceDate) - let direction = WeightTrendEvaluator.direction( - weights: weights, - windowStart: windowStart, - referenceDate: referenceDate - ) - save(direction, userDefaults: userDefaults) - } -} - -extension WeightTrendDirection { - var storedValue: String { - switch self { - case .up: "up" - case .down: "down" - case .flat: "flat" - case .unknown: "unknown" - } - } - - init(storedValue: String) { - switch storedValue { - case "up": self = .up - case "down": self = .down - case "flat": self = .flat - default: self = .unknown - } - } -} diff --git a/Packages/LogWeightCore/Tests/LogWeightCoreTests/TrendArrowPreferencesTests.swift b/Packages/LogWeightCore/Tests/LogWeightCoreTests/TrendArrowPreferencesTests.swift deleted file mode 100644 index cb7f1c4..0000000 --- a/Packages/LogWeightCore/Tests/LogWeightCoreTests/TrendArrowPreferencesTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -import XCTest -@testable import LogWeightCore - -final class TrendArrowPreferencesTests: XCTestCase { - func testIsEnabledDefaultsToTrueWhenUnset() { - let shared = UserDefaults(suiteName: "TrendArrowPreferencesTests.unset.shared")! - let standard = UserDefaults(suiteName: "TrendArrowPreferencesTests.unset.standard")! - shared.removePersistentDomain(forName: "TrendArrowPreferencesTests.unset.shared") - standard.removePersistentDomain(forName: "TrendArrowPreferencesTests.unset.standard") - - XCTAssertTrue(TrendArrowPreferences.isEnabled(sharedDefaults: shared, standardDefaults: standard)) - } - - func testIsEnabledReadsSharedDefaultsFirst() { - let shared = UserDefaults(suiteName: "TrendArrowPreferencesTests.shared")! - let standard = UserDefaults(suiteName: "TrendArrowPreferencesTests.standard")! - shared.removePersistentDomain(forName: "TrendArrowPreferencesTests.shared") - standard.removePersistentDomain(forName: "TrendArrowPreferencesTests.standard") - - shared.set(false, forKey: SettingsKey.trendArrowEnabled) - standard.set(true, forKey: SettingsKey.trendArrowEnabled) - - XCTAssertFalse(TrendArrowPreferences.isEnabled(sharedDefaults: shared, standardDefaults: standard)) - } - - func testMirrorToAppGroup() { - let shared = UserDefaults(suiteName: "TrendArrowPreferencesTests.mirror")! - let standard = UserDefaults(suiteName: "TrendArrowPreferencesTests.mirrorStandard")! - shared.removePersistentDomain(forName: "TrendArrowPreferencesTests.mirror") - standard.removePersistentDomain(forName: "TrendArrowPreferencesTests.mirrorStandard") - - standard.set(false, forKey: SettingsKey.trendArrowEnabled) - TrendArrowPreferences.mirrorToAppGroup(standardDefaults: standard, sharedDefaults: shared) - - XCTAssertFalse(shared.bool(forKey: SettingsKey.trendArrowEnabled)) - } -} diff --git a/Packages/LogWeightCore/Tests/LogWeightCoreTests/WeightTrendCacheTests.swift b/Packages/LogWeightCore/Tests/LogWeightCoreTests/WeightTrendCacheTests.swift deleted file mode 100644 index ebb65b0..0000000 --- a/Packages/LogWeightCore/Tests/LogWeightCoreTests/WeightTrendCacheTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -import XCTest -@testable import LogWeightCore - -final class WeightTrendCacheTests: XCTestCase { - private var defaults: UserDefaults! - - override func setUp() { - super.setUp() - let suiteName = "WeightTrendCacheTests.\(UUID().uuidString)" - defaults = UserDefaults(suiteName: suiteName)! - defaults.removePersistentDomain(forName: suiteName) - } - - func testSaveAndLoadRoundTrip() { - WeightTrendCache.save(.up, userDefaults: defaults) - XCTAssertEqual(WeightTrendCache.load(userDefaults: defaults), .up) - } - - func testLoadWhenMissingReturnsUnknown() { - XCTAssertEqual(WeightTrendCache.load(userDefaults: defaults), .unknown) - } - - func testUpdateFromWeightsPersistsDirection() { - let reference = ScreenshotFixture.referenceDate - let weights = ScreenshotFixture.linearTrend30Days.samples(now: reference) - WeightTrendCache.update(from: weights, referenceDate: reference, userDefaults: defaults) - XCTAssertEqual(WeightTrendCache.load(userDefaults: defaults), .down) - } -} diff --git a/Packages/LogWeightCore/Tests/LogWeightCoreTests/WeightTrendEvaluatorTests.swift b/Packages/LogWeightCore/Tests/LogWeightCoreTests/WeightTrendEvaluatorTests.swift deleted file mode 100644 index cf1e227..0000000 --- a/Packages/LogWeightCore/Tests/LogWeightCoreTests/WeightTrendEvaluatorTests.swift +++ /dev/null @@ -1,146 +0,0 @@ -import XCTest -@testable import LogWeightCore - -final class WeightTrendEvaluatorTests: XCTestCase { - private let reference = ScreenshotFixture.referenceDate - - func testLinearTrend30DaysIsDown() { - let samples = ScreenshotFixture.linearTrend30Days.samples(now: reference) - let windowStart = Calendar.current.date(byAdding: .day, value: -28, to: reference) - - let direction = WeightTrendEvaluator.direction( - weights: samples, - windowStart: windowStart, - referenceDate: reference - ) - - XCTAssertEqual(direction, .down) - } - - func testTooFewSamplesReturnsUnknown() { - let samples = [ - Weight(valueInKilograms: 75.0, recordedAt: reference.addingTimeInterval(-3 * 86_400)), - Weight(valueInKilograms: 74.5, recordedAt: reference) - ] - let windowStart = Calendar.current.date(byAdding: .day, value: -28, to: reference) - - let direction = WeightTrendEvaluator.direction( - weights: samples, - windowStart: windowStart, - referenceDate: reference - ) - - XCTAssertEqual(direction, .unknown) - } - - func testSpanTooShortReturnsUnknown() { - let samples = (0..<4).map { index in - Weight( - valueInKilograms: 75.0 + Double(index) * 0.5, - recordedAt: reference.addingTimeInterval(Double(index) * 86_400) - ) - } - let windowStart = Calendar.current.date(byAdding: .day, value: -28, to: reference) - - let direction = WeightTrendEvaluator.direction( - weights: samples, - windowStart: windowStart, - referenceDate: reference - ) - - XCTAssertEqual(direction, .unknown) - } - - func testFlatPlateauReturnsFlat() { - let calendar = Calendar(identifier: .gregorian) - let samples = (0..<10).map { offset in - let date = calendar.date(byAdding: .day, value: -(9 - offset) * 3, to: reference) ?? reference - return Weight(valueInKilograms: 75.0, recordedAt: date) - } - let windowStart = calendar.date(byAdding: .day, value: -28, to: reference) - - let direction = WeightTrendEvaluator.direction( - weights: samples, - windowStart: windowStart, - referenceDate: reference - ) - - XCTAssertEqual(direction, .flat) - } - - func testClearUpwardTrend() { - let calendar = Calendar(identifier: .gregorian) - let samples = (0..<8).map { offset in - let date = calendar.date(byAdding: .day, value: -(7 - offset) * 4, to: reference) ?? reference - return Weight(valueInKilograms: 70.0 + Double(offset) * 0.4, recordedAt: date) - } - let windowStart = calendar.date(byAdding: .day, value: -28, to: reference) - - let direction = WeightTrendEvaluator.direction( - weights: samples, - windowStart: windowStart, - referenceDate: reference - ) - - XCTAssertEqual(direction, .up) - } - - func testSamplesOutsideWindowAreIgnored() { - let calendar = Calendar(identifier: .gregorian) - let old = Weight( - valueInKilograms: 90.0, - recordedAt: calendar.date(byAdding: .day, value: -60, to: reference) ?? reference - ) - let recent = (0..<5).map { offset in - let date = calendar.date(byAdding: .day, value: -(4 - offset) * 5, to: reference) ?? reference - return Weight(valueInKilograms: 75.0 + Double(offset) * 0.5, recordedAt: date) - } - let windowStart = calendar.date(byAdding: .day, value: -28, to: reference) - - let direction = WeightTrendEvaluator.direction( - weights: [old] + recent, - windowStart: windowStart, - referenceDate: reference - ) - - XCTAssertEqual(direction, .up) - } - - func testOneWeekChartConfigurationAllowsShortSpan() { - let calendar = Calendar(identifier: .gregorian) - let samples = [ - Weight( - valueInKilograms: 72.0, - recordedAt: calendar.date(byAdding: .day, value: -2, to: reference) ?? reference - ), - Weight(valueInKilograms: 67.9, recordedAt: reference) - ] - let windowStart = ChartTimeRange.oneWeek.cutoffDate(referenceDate: reference) - - let direction = WeightTrendEvaluator.direction( - weights: samples, - windowStart: windowStart, - referenceDate: reference, - configuration: .forChartRange(.oneWeek) - ) - - XCTAssertEqual(direction, .down) - } - - func testFutureSamplesExcluded() { - let future = Weight( - valueInKilograms: 100.0, - recordedAt: reference.addingTimeInterval(86_400) - ) - let samples = ScreenshotFixture.linearTrend30Days.samples(now: reference) + [future] - let windowStart = Calendar.current.date(byAdding: .day, value: -28, to: reference) - - let direction = WeightTrendEvaluator.direction( - weights: samples, - windowStart: windowStart, - referenceDate: reference - ) - - XCTAssertEqual(direction, .down) - } -} diff --git a/project.yml b/project.yml index 90cab9a..c2493c1 100644 --- a/project.yml +++ b/project.yml @@ -71,7 +71,6 @@ targets: - "Resources/Info.plist" - "Resources/LogWeightWatchWidget.entitlements" - "Resources/PrivacyInfo.xcprivacy" - - path: App/Shared/Widget/WeightTrendArrow.swift resources: - path: App/Shared/Resources - path: App/WatchWidget/Resources/PrivacyInfo.xcprivacy