From 7b94084408425f16f1013d4c1a081cb5e32eb2d4 Mon Sep 17 00:00:00 2001 From: titus Date: Mon, 4 May 2026 20:57:10 +0200 Subject: [PATCH 1/6] new hrm graph --- InfiniLink.xcodeproj/project.pbxproj | 12 +- .../Charts/Heart/HeartChartView.swift | 155 ++++++++++++++---- InfiniLink/InfiniLink.entitlements | 20 +-- InfiniLink/Info.plist | 4 - InfiniLink/Localizable.xcstrings | 29 ++-- InfiniLink/Utils/ChartManager.swift | 2 +- InfiniLink/Utils/HealthKitManager.swift | 4 +- 7 files changed, 147 insertions(+), 79 deletions(-) diff --git a/InfiniLink.xcodeproj/project.pbxproj b/InfiniLink.xcodeproj/project.pbxproj index d1a4ede..7d69096 100644 --- a/InfiniLink.xcodeproj/project.pbxproj +++ b/InfiniLink.xcodeproj/project.pbxproj @@ -1029,17 +1029,17 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"InfiniLink/Preview Content\""; - DEVELOPMENT_TEAM = UXA3H4X42S; + DEVELOPMENT_TEAM = 6H5DHLRX53; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = InfiniLink/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 16; + IPHONEOS_DEPLOYMENT_TARGET = 17; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = "com.alexemry.Infini-iOS"; + PRODUCT_BUNDLE_IDENTIFIER = com.titusk.InfiniLink; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1061,17 +1061,17 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"InfiniLink/Preview Content\""; - DEVELOPMENT_TEAM = UXA3H4X42S; + DEVELOPMENT_TEAM = 6H5DHLRX53; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = InfiniLink/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 16; + IPHONEOS_DEPLOYMENT_TARGET = 17; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = "com.alexemry.Infini-iOS"; + PRODUCT_BUNDLE_IDENTIFIER = com.titusk.InfiniLink; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift b/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift index 3edf34c..5f3afa5 100644 --- a/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift +++ b/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift @@ -11,7 +11,10 @@ import Charts struct HeartChartDataPoint: Identifiable { var id = UUID() let date: Date - let value: Double + let min: Double + let max: Double + let average: Double + let values: [Double] } struct HeartChartView: View { @@ -22,21 +25,92 @@ struct HeartChartView: View { @AppStorage("maxHeartRange") private var maxHeartRange = 200 @State private var points = [HeartChartDataPoint]() + @State private var scrollPosition: Date = Date(timeInterval: -86400, since: Date()) + @State private var displayedDate: Date = Date() + @State private var displayedMin: Int = 0 + @State private var displayedMax: Int = 0 + + var visiblePoints: [HeartChartDataPoint] { + let windowStart = scrollPosition + let windowEnd = Date(timeInterval: 86400, since: scrollPosition) + return points.filter { $0.date >= windowStart && $0.date <= windowEnd } + } + + var visibleMax: Int { + Int(visiblePoints.map({ $0.max }).max() ?? 200) + } + var visibleMin: Int { + Int(visiblePoints.map({ $0.min }).min() ?? 0) + } func heartPoints() -> [HeartChartDataPoint] { - return ChartManager.shared.heartPoints().map { HeartChartDataPoint(date: $0.timestamp ?? Date(), value: $0.value) } + let raw = ChartManager.shared.heartPoints() + + let grouped = Dictionary(grouping: raw) { sample -> Date in + let comps = Calendar.current.dateComponents([.year, .month, .day, .hour], from: sample.timestamp ?? Date()) + return Calendar.current.date(from: comps) ?? Date() + } + + return grouped.map { (bucket, samples) in + let values = samples.map { $0.value } + return HeartChartDataPoint( + date: bucket, + min: values.min() ?? 0, + max: values.max() ?? 0, + average: values.reduce(0, +) / Double(values.count), + values: values + ) + }.sorted { $0.date < $1.date } } + var earliestDate: Date { - return points.compactMap({ $0.date }).min() ?? Date() + points.map({ $0.date }).min() ?? Date() } var latestDate: Date { - return points.compactMap({ $0.date }).max() ?? Date() + points.map({ $0.date }).max() ?? Date() + } + var overallMax: Int { + Int(points.map({ $0.max }).max() ?? 0) } - var max: Int { - return Int(points.compactMap({ $0.value }).max() ?? 0) + var overallMin: Int { + Int(points.map({ $0.min }).min() ?? 0) + } + + let heartColor = Color(red: 0.996, green: 0.212, blue: 0.369) + let darkHeartColor = Color(red: 0.369, green: 0.090, blue: 0.145) + + func isSingleReading(_ point: HeartChartDataPoint) -> Bool { + point.min == point.max } - var min: Int { - return Int(points.compactMap({ $0.value }).min() ?? 0) + + @ChartContentBuilder + func chartContent(for point: HeartChartDataPoint) -> some ChartContent { + if isSingleReading(point) { + PointMark( + x: .value("Time", point.date), + y: .value("BPM", point.min) + ) + .foregroundStyle(heartColor) + .symbolSize(40) + .symbol(.circle) + } else { + RectangleMark( + x: .value("Time", point.date), + yStart: .value("Min", point.min), + yEnd: .value("Max", point.max), + width: 7 + ) + .foregroundStyle(darkHeartColor) + .clipShape(Capsule()) + + PointMark( + x: .value("Time", point.date), + y: .value("BPM", point.average) + ) + .foregroundStyle(heartColor) + .symbolSize(CGSize(width: 7, height: 7)) + .symbol(.circle) + } } var body: some View { @@ -46,30 +120,44 @@ struct HeartChartView: View { EmptyChartView(.heart) } else { Section { - Chart(points) { point in - PointMark( - x: .value("Time", point.date), - y: .value("BPM", point.value) - ) - .clipShape(Capsule()) - .foregroundStyle(Color.red) + Chart { + ForEach(points) { point in + chartContent(for: point) + } } .frame(height: 280) - .chartYScale(domain: minHeartRange...maxHeartRange) + .chartYScale(domain: (displayedMin - 20)...(displayedMax + 20)) + .chartXAxis { + AxisMarks(values: .stride(by: .hour, count: 6)) { value in + AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [4])) + AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .omitted))) + } + } + .chartScrollableAxes(.horizontal) + .chartXVisibleDomain(length: 86400) + .chartScrollPosition(x: $scrollPosition) + .onChange(of: scrollPosition) { newValue in + Task { + try? await Task.sleep(nanoseconds: 300_000_000) + if scrollPosition == newValue { + displayedDate = newValue + displayedMin = visibleMin + displayedMax = visibleMax + } + } + } } header: { VStack(alignment: .leading) { - Text(points.count > 1 ? "Range" : "No Data") - Text({ - if max == 0 || min == 0 { - return "0 " - } else { - return "\(min)-\(max) " - } - }()) - .font(.system(.title, design: .rounded)) - .foregroundColor(.primary) + Text("Range") + .font(.caption) + .foregroundColor(.secondary) + Text(displayedMax == 0 || displayedMin == 0 ? "0 " : "\(displayedMin)–\(displayedMax) ") + .font(.system(.title, design: .rounded)) + .foregroundColor(.primary) + Text("BPM") - Text("\(earliestDate.formatted(.dateTime.month(.abbreviated).day()))-\(latestDate.formatted(.dateTime.day()))") + Text(displayedDate.formatted(.dateTime.weekday(.abbreviated).month(.abbreviated).day().year())) + .foregroundColor(.secondary) + .font(.subheadline) } .fontWeight(.semibold) } @@ -79,16 +167,25 @@ struct HeartChartView: View { .listRowBackground(Color.clear) if points.count >= 3 { Section { - Text("Today your heart rate reached a high of \(max), and dropped to a low of \(min) BPM.") - // Text("Is a heart point in an exercise in the last day: \(ExerciseViewModel.shared.isDateDuringExercise(Date()))") + Text("Today your heart rate reached a high of \(displayedMax), and dropped to a low of \(displayedMin) BPM.") } } } .onAppear { points = heartPoints() + scrollPosition = Date(timeInterval: -86400, since: latestDate) + displayedDate = latestDate + displayedMin = visibleMin + displayedMax = visibleMax } .onChange(of: bleManager.heartRate) { _ in points = heartPoints() } } } + +#Preview { + List { + HeartChartView() + } +} diff --git a/InfiniLink/InfiniLink.entitlements b/InfiniLink/InfiniLink.entitlements index c5f2828..cce046c 100644 --- a/InfiniLink/InfiniLink.entitlements +++ b/InfiniLink/InfiniLink.entitlements @@ -2,28 +2,10 @@ - aps-environment - development - com.apple.developer.healthkit - - com.apple.developer.healthkit.background-delivery - - com.apple.developer.icloud-container-identifiers - - iCloud.com.alexemry.Infini-iOS - - com.apple.developer.icloud-services - - CloudKit - - com.apple.developer.weatherkit - com.apple.security.app-sandbox com.apple.security.application-groups - - group.com.alexemry.Infini-iOS - + com.apple.security.device.bluetooth com.apple.security.files.user-selected.read-only diff --git a/InfiniLink/Info.plist b/InfiniLink/Info.plist index 22d3446..82b78dd 100644 --- a/InfiniLink/Info.plist +++ b/InfiniLink/Info.plist @@ -36,10 +36,6 @@ This app uses bluetooth to communicate with your PineTime. NSCalendarsUsageDescription This app needs access to your calendars to receive event reminders on your watch. - NSHealthShareUsageDescription - This app optionally uses Apple Health to write your heart rate and step count data. - NSHealthUpdateUsageDescription - This app optionally uses Apple Health to write your heart rate and step count data. NSLocationAlwaysAndWhenInUseUsageDescription This app needs your location to receive local weather information when the app is in the background. NSLocationAlwaysUsageDescription diff --git a/InfiniLink/Localizable.xcstrings b/InfiniLink/Localizable.xcstrings index 3ae3ebb..29e0b34 100644 --- a/InfiniLink/Localizable.xcstrings +++ b/InfiniLink/Localizable.xcstrings @@ -96,16 +96,6 @@ } } }, - "%@-%@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$@-%2$@" - } - } - } - }, "%@:%@:%@" : { "localizations" : { "en" : { @@ -174,18 +164,18 @@ } } }, - "%lld-%lld " : { + "%lld-day" : { + + }, + "%lld–%lld " : { "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "%1$lld-%2$lld " + "value" : "%1$lld–%2$lld " } } } - }, - "%lld-day" : { - }, "%lld%%" : { @@ -805,6 +795,9 @@ } } } + }, + "Max" : { + }, "Maximum" : { "localizations" : { @@ -818,6 +811,9 @@ }, "Metric" : { + }, + "Min" : { + }, "Minimum" : { @@ -855,9 +851,6 @@ }, "No" : { - }, - "No Data" : { - }, "No Logs" : { diff --git a/InfiniLink/Utils/ChartManager.swift b/InfiniLink/Utils/ChartManager.swift index e3bf5cd..9205acd 100644 --- a/InfiniLink/Utils/ChartManager.swift +++ b/InfiniLink/Utils/ChartManager.swift @@ -108,7 +108,7 @@ class ChartManager: ObservableObject { func heartPoints(predicate: NSPredicate? = nil) -> [HeartDataPoint] { let fetchRequest: NSFetchRequest = HeartDataPoint.fetchRequest() - fetchRequest.predicate = predicate ?? dayPredicate + fetchRequest.predicate = predicate ?? weekPredicate do { return try persistenceController.container.viewContext.fetch(fetchRequest) diff --git a/InfiniLink/Utils/HealthKitManager.swift b/InfiniLink/Utils/HealthKitManager.swift index 7a0e702..9c2e70e 100644 --- a/InfiniLink/Utils/HealthKitManager.swift +++ b/InfiniLink/Utils/HealthKitManager.swift @@ -91,10 +91,10 @@ class HealthKitManager: ObservableObject { guard let healthStore = self.healthStore else { return } - healthStore.requestAuthorization(toShare: [stepsType, heartRateType, caloriesType, workoutType], read: [stepsType, heartRateType]) { success, error in + /*healthStore.requestAuthorization(toShare: [stepsType, heartRateType, caloriesType, workoutType], read: [stepsType, heartRateType]) { success, error in if let error = error { log(error.localizedDescription, caller: "HealthKitManager") } - } + }*/ } } From 4991da0ed5d758feabb6e95de45e6ba412797afe Mon Sep 17 00:00:00 2001 From: titus Date: Tue, 5 May 2026 05:57:44 +0200 Subject: [PATCH 2/6] change scrollable to switchable because of ios 16 --- .../Charts/Heart/HeartChartView.swift | 109 ++++++++++-------- 1 file changed, 64 insertions(+), 45 deletions(-) diff --git a/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift b/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift index 5f3afa5..803ea76 100644 --- a/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift +++ b/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift @@ -25,22 +25,19 @@ struct HeartChartView: View { @AppStorage("maxHeartRange") private var maxHeartRange = 200 @State private var points = [HeartChartDataPoint]() - @State private var scrollPosition: Date = Date(timeInterval: -86400, since: Date()) + @State private var dayOffset: Int = 0 @State private var displayedDate: Date = Date() @State private var displayedMin: Int = 0 @State private var displayedMax: Int = 0 - var visiblePoints: [HeartChartDataPoint] { - let windowStart = scrollPosition - let windowEnd = Date(timeInterval: 86400, since: scrollPosition) - return points.filter { $0.date >= windowStart && $0.date <= windowEnd } + var windowStart: Date { + Calendar.current.startOfDay(for: Calendar.current.date(byAdding: .day, value: dayOffset, to: Date())!) } - - var visibleMax: Int { - Int(visiblePoints.map({ $0.max }).max() ?? 200) + var windowEnd: Date { + Date(timeInterval: 86400, since: windowStart) } - var visibleMin: Int { - Int(visiblePoints.map({ $0.min }).min() ?? 0) + var windowPoints: [HeartChartDataPoint] { + points.filter { $0.date >= windowStart && $0.date <= windowEnd } } func heartPoints() -> [HeartChartDataPoint] { @@ -69,12 +66,6 @@ struct HeartChartView: View { var latestDate: Date { points.map({ $0.date }).max() ?? Date() } - var overallMax: Int { - Int(points.map({ $0.max }).max() ?? 0) - } - var overallMin: Int { - Int(points.map({ $0.min }).min() ?? 0) - } let heartColor = Color(red: 0.996, green: 0.212, blue: 0.369) let darkHeartColor = Color(red: 0.369, green: 0.090, blue: 0.145) @@ -113,6 +104,12 @@ struct HeartChartView: View { } } + func updateDisplayed() { + displayedDate = windowStart + displayedMin = Int(windowPoints.map({ $0.min }).min() ?? 0) + displayedMax = Int(windowPoints.map({ $0.max }).max() ?? 0) + } + var body: some View { Group { Group { @@ -120,29 +117,53 @@ struct HeartChartView: View { EmptyChartView(.heart) } else { Section { - Chart { - ForEach(points) { point in - chartContent(for: point) + VStack(spacing: 0) { + HStack { + Button { + dayOffset -= 1 + } label: { + Image(systemName: "chevron.left") + } + .disabled(windowStart <= earliestDate) + + Spacer() + + Text(displayedDate.formatted(.dateTime.weekday(.abbreviated).month(.abbreviated).day().year())) + .foregroundColor(.primary) + + Spacer() + + Button { + dayOffset += 1 + } label: { + Image(systemName: "chevron.right") + } + .disabled(dayOffset >= 0) } - } - .frame(height: 280) - .chartYScale(domain: (displayedMin - 20)...(displayedMax + 20)) - .chartXAxis { - AxisMarks(values: .stride(by: .hour, count: 6)) { value in - AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [4])) - AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .omitted))) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(Color(.secondarySystemGroupedBackground)) + .clipShape(Capsule()) + .padding(.bottom, 8) + + Chart { + ForEach(windowPoints) { point in + chartContent(for: point) + } } - } - .chartScrollableAxes(.horizontal) - .chartXVisibleDomain(length: 86400) - .chartScrollPosition(x: $scrollPosition) - .onChange(of: scrollPosition) { newValue in - Task { - try? await Task.sleep(nanoseconds: 300_000_000) - if scrollPosition == newValue { - displayedDate = newValue - displayedMin = visibleMin - displayedMax = visibleMax + .frame(height: 280) + .chartYScale(domain: (displayedMin - 20)...(displayedMax + 20)) + .chartXScale(domain: windowStart...windowEnd) + .chartXAxis { + AxisMarks(values: .stride(by: .hour, count: 6)) { value in + AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [4])) + AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .omitted))) + } + } + .chartYAxis { + AxisMarks(position: .trailing) { value in + AxisGridLine() + AxisValueLabel() } } } @@ -155,9 +176,6 @@ struct HeartChartView: View { .font(.system(.title, design: .rounded)) .foregroundColor(.primary) + Text("BPM") - Text(displayedDate.formatted(.dateTime.weekday(.abbreviated).month(.abbreviated).day().year())) - .foregroundColor(.secondary) - .font(.subheadline) } .fontWeight(.semibold) } @@ -173,13 +191,14 @@ struct HeartChartView: View { } .onAppear { points = heartPoints() - scrollPosition = Date(timeInterval: -86400, since: latestDate) - displayedDate = latestDate - displayedMin = visibleMin - displayedMax = visibleMax + updateDisplayed() + } + .onChange(of: dayOffset) { _, _ in + updateDisplayed() } - .onChange(of: bleManager.heartRate) { _ in + .onChange(of: bleManager.heartRate) { _, _ in points = heartPoints() + updateDisplayed() } } } From 95de91d9b5943f887266dd201bc5d3fbe8a3277a Mon Sep 17 00:00:00 2001 From: titus Date: Tue, 5 May 2026 06:17:16 +0200 Subject: [PATCH 3/6] Revert local build dependency changes --- InfiniLink.xcodeproj/project.pbxproj | 12 +++++----- InfiniLink/InfiniLink.entitlements | 20 ++++++++++++++++- InfiniLink/Info.plist | 4 ++++ InfiniLink/Localizable.xcstrings | 29 +++++++++++++++---------- InfiniLink/Utils/HealthKitManager.swift | 4 ++-- 5 files changed, 49 insertions(+), 20 deletions(-) diff --git a/InfiniLink.xcodeproj/project.pbxproj b/InfiniLink.xcodeproj/project.pbxproj index 7d69096..d1a4ede 100644 --- a/InfiniLink.xcodeproj/project.pbxproj +++ b/InfiniLink.xcodeproj/project.pbxproj @@ -1029,17 +1029,17 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"InfiniLink/Preview Content\""; - DEVELOPMENT_TEAM = 6H5DHLRX53; + DEVELOPMENT_TEAM = UXA3H4X42S; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = InfiniLink/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 17; + IPHONEOS_DEPLOYMENT_TARGET = 16; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = com.titusk.InfiniLink; + PRODUCT_BUNDLE_IDENTIFIER = "com.alexemry.Infini-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1061,17 +1061,17 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"InfiniLink/Preview Content\""; - DEVELOPMENT_TEAM = 6H5DHLRX53; + DEVELOPMENT_TEAM = UXA3H4X42S; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = InfiniLink/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 17; + IPHONEOS_DEPLOYMENT_TARGET = 16; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = com.titusk.InfiniLink; + PRODUCT_BUNDLE_IDENTIFIER = "com.alexemry.Infini-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/InfiniLink/InfiniLink.entitlements b/InfiniLink/InfiniLink.entitlements index cce046c..c5f2828 100644 --- a/InfiniLink/InfiniLink.entitlements +++ b/InfiniLink/InfiniLink.entitlements @@ -2,10 +2,28 @@ + aps-environment + development + com.apple.developer.healthkit + + com.apple.developer.healthkit.background-delivery + + com.apple.developer.icloud-container-identifiers + + iCloud.com.alexemry.Infini-iOS + + com.apple.developer.icloud-services + + CloudKit + + com.apple.developer.weatherkit + com.apple.security.app-sandbox com.apple.security.application-groups - + + group.com.alexemry.Infini-iOS + com.apple.security.device.bluetooth com.apple.security.files.user-selected.read-only diff --git a/InfiniLink/Info.plist b/InfiniLink/Info.plist index 82b78dd..22d3446 100644 --- a/InfiniLink/Info.plist +++ b/InfiniLink/Info.plist @@ -36,6 +36,10 @@ This app uses bluetooth to communicate with your PineTime. NSCalendarsUsageDescription This app needs access to your calendars to receive event reminders on your watch. + NSHealthShareUsageDescription + This app optionally uses Apple Health to write your heart rate and step count data. + NSHealthUpdateUsageDescription + This app optionally uses Apple Health to write your heart rate and step count data. NSLocationAlwaysAndWhenInUseUsageDescription This app needs your location to receive local weather information when the app is in the background. NSLocationAlwaysUsageDescription diff --git a/InfiniLink/Localizable.xcstrings b/InfiniLink/Localizable.xcstrings index 29e0b34..3ae3ebb 100644 --- a/InfiniLink/Localizable.xcstrings +++ b/InfiniLink/Localizable.xcstrings @@ -96,6 +96,16 @@ } } }, + "%@-%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@-%2$@" + } + } + } + }, "%@:%@:%@" : { "localizations" : { "en" : { @@ -164,18 +174,18 @@ } } }, - "%lld-day" : { - - }, - "%lld–%lld " : { + "%lld-%lld " : { "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "%1$lld–%2$lld " + "value" : "%1$lld-%2$lld " } } } + }, + "%lld-day" : { + }, "%lld%%" : { @@ -795,9 +805,6 @@ } } } - }, - "Max" : { - }, "Maximum" : { "localizations" : { @@ -811,9 +818,6 @@ }, "Metric" : { - }, - "Min" : { - }, "Minimum" : { @@ -851,6 +855,9 @@ }, "No" : { + }, + "No Data" : { + }, "No Logs" : { diff --git a/InfiniLink/Utils/HealthKitManager.swift b/InfiniLink/Utils/HealthKitManager.swift index 9c2e70e..7a0e702 100644 --- a/InfiniLink/Utils/HealthKitManager.swift +++ b/InfiniLink/Utils/HealthKitManager.swift @@ -91,10 +91,10 @@ class HealthKitManager: ObservableObject { guard let healthStore = self.healthStore else { return } - /*healthStore.requestAuthorization(toShare: [stepsType, heartRateType, caloriesType, workoutType], read: [stepsType, heartRateType]) { success, error in + healthStore.requestAuthorization(toShare: [stepsType, heartRateType, caloriesType, workoutType], read: [stepsType, heartRateType]) { success, error in if let error = error { log(error.localizedDescription, caller: "HealthKitManager") } - }*/ + } } } From d8f307d135290841e385cf797b4c71834ac6ee8a Mon Sep 17 00:00:00 2001 From: titus Date: Tue, 5 May 2026 08:49:34 +0200 Subject: [PATCH 4/6] fix button behavior and ios 16 compatibility issues --- InfiniLink.xcodeproj/project.pbxproj | 8 ++--- .../Charts/Heart/HeartChartView.swift | 7 +++-- InfiniLink/InfiniLink.entitlements | 20 +------------ InfiniLink/Info.plist | 4 --- InfiniLink/Localizable.xcstrings | 29 +++++++------------ InfiniLink/Utils/HealthKitManager.swift | 4 +-- 6 files changed, 22 insertions(+), 50 deletions(-) diff --git a/InfiniLink.xcodeproj/project.pbxproj b/InfiniLink.xcodeproj/project.pbxproj index d1a4ede..db78b73 100644 --- a/InfiniLink.xcodeproj/project.pbxproj +++ b/InfiniLink.xcodeproj/project.pbxproj @@ -1029,7 +1029,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"InfiniLink/Preview Content\""; - DEVELOPMENT_TEAM = UXA3H4X42S; + DEVELOPMENT_TEAM = 6H5DHLRX53; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = InfiniLink/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -1039,7 +1039,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = "com.alexemry.Infini-iOS"; + PRODUCT_BUNDLE_IDENTIFIER = com.titusk.InfiniLink; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1061,7 +1061,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"InfiniLink/Preview Content\""; - DEVELOPMENT_TEAM = UXA3H4X42S; + DEVELOPMENT_TEAM = 6H5DHLRX53; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = InfiniLink/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -1071,7 +1071,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = "com.alexemry.Infini-iOS"; + PRODUCT_BUNDLE_IDENTIFIER = com.titusk.InfiniLink; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift b/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift index 803ea76..f3746ea 100644 --- a/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift +++ b/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift @@ -124,7 +124,7 @@ struct HeartChartView: View { } label: { Image(systemName: "chevron.left") } - .disabled(windowStart <= earliestDate) + .disabled(Calendar.current.isDate(windowStart, inSameDayAs: earliestDate)) Spacer() @@ -167,6 +167,7 @@ struct HeartChartView: View { } } } + .buttonStyle(.plain) } header: { VStack(alignment: .leading) { Text("Range") @@ -193,10 +194,10 @@ struct HeartChartView: View { points = heartPoints() updateDisplayed() } - .onChange(of: dayOffset) { _, _ in + .onChange(of: dayOffset) { _ in updateDisplayed() } - .onChange(of: bleManager.heartRate) { _, _ in + .onChange(of: bleManager.heartRate) { _ in points = heartPoints() updateDisplayed() } diff --git a/InfiniLink/InfiniLink.entitlements b/InfiniLink/InfiniLink.entitlements index c5f2828..cce046c 100644 --- a/InfiniLink/InfiniLink.entitlements +++ b/InfiniLink/InfiniLink.entitlements @@ -2,28 +2,10 @@ - aps-environment - development - com.apple.developer.healthkit - - com.apple.developer.healthkit.background-delivery - - com.apple.developer.icloud-container-identifiers - - iCloud.com.alexemry.Infini-iOS - - com.apple.developer.icloud-services - - CloudKit - - com.apple.developer.weatherkit - com.apple.security.app-sandbox com.apple.security.application-groups - - group.com.alexemry.Infini-iOS - + com.apple.security.device.bluetooth com.apple.security.files.user-selected.read-only diff --git a/InfiniLink/Info.plist b/InfiniLink/Info.plist index 22d3446..82b78dd 100644 --- a/InfiniLink/Info.plist +++ b/InfiniLink/Info.plist @@ -36,10 +36,6 @@ This app uses bluetooth to communicate with your PineTime. NSCalendarsUsageDescription This app needs access to your calendars to receive event reminders on your watch. - NSHealthShareUsageDescription - This app optionally uses Apple Health to write your heart rate and step count data. - NSHealthUpdateUsageDescription - This app optionally uses Apple Health to write your heart rate and step count data. NSLocationAlwaysAndWhenInUseUsageDescription This app needs your location to receive local weather information when the app is in the background. NSLocationAlwaysUsageDescription diff --git a/InfiniLink/Localizable.xcstrings b/InfiniLink/Localizable.xcstrings index 3ae3ebb..29e0b34 100644 --- a/InfiniLink/Localizable.xcstrings +++ b/InfiniLink/Localizable.xcstrings @@ -96,16 +96,6 @@ } } }, - "%@-%@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$@-%2$@" - } - } - } - }, "%@:%@:%@" : { "localizations" : { "en" : { @@ -174,18 +164,18 @@ } } }, - "%lld-%lld " : { + "%lld-day" : { + + }, + "%lld–%lld " : { "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "%1$lld-%2$lld " + "value" : "%1$lld–%2$lld " } } } - }, - "%lld-day" : { - }, "%lld%%" : { @@ -805,6 +795,9 @@ } } } + }, + "Max" : { + }, "Maximum" : { "localizations" : { @@ -818,6 +811,9 @@ }, "Metric" : { + }, + "Min" : { + }, "Minimum" : { @@ -855,9 +851,6 @@ }, "No" : { - }, - "No Data" : { - }, "No Logs" : { diff --git a/InfiniLink/Utils/HealthKitManager.swift b/InfiniLink/Utils/HealthKitManager.swift index 7a0e702..9c2e70e 100644 --- a/InfiniLink/Utils/HealthKitManager.swift +++ b/InfiniLink/Utils/HealthKitManager.swift @@ -91,10 +91,10 @@ class HealthKitManager: ObservableObject { guard let healthStore = self.healthStore else { return } - healthStore.requestAuthorization(toShare: [stepsType, heartRateType, caloriesType, workoutType], read: [stepsType, heartRateType]) { success, error in + /*healthStore.requestAuthorization(toShare: [stepsType, heartRateType, caloriesType, workoutType], read: [stepsType, heartRateType]) { success, error in if let error = error { log(error.localizedDescription, caller: "HealthKitManager") } - } + }*/ } } From 0d6a2b437664adac0ef24863fe71da54b19df77a Mon Sep 17 00:00:00 2001 From: titus Date: Tue, 5 May 2026 08:51:52 +0200 Subject: [PATCH 5/6] revert local files... again --- InfiniLink.xcodeproj/project.pbxproj | 8 +++---- InfiniLink/InfiniLink.entitlements | 20 ++++++++++++++++- InfiniLink/Info.plist | 4 ++++ InfiniLink/Localizable.xcstrings | 29 +++++++++++++++---------- InfiniLink/Utils/HealthKitManager.swift | 4 ++-- 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/InfiniLink.xcodeproj/project.pbxproj b/InfiniLink.xcodeproj/project.pbxproj index db78b73..d1a4ede 100644 --- a/InfiniLink.xcodeproj/project.pbxproj +++ b/InfiniLink.xcodeproj/project.pbxproj @@ -1029,7 +1029,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"InfiniLink/Preview Content\""; - DEVELOPMENT_TEAM = 6H5DHLRX53; + DEVELOPMENT_TEAM = UXA3H4X42S; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = InfiniLink/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -1039,7 +1039,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = com.titusk.InfiniLink; + PRODUCT_BUNDLE_IDENTIFIER = "com.alexemry.Infini-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1061,7 +1061,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"InfiniLink/Preview Content\""; - DEVELOPMENT_TEAM = 6H5DHLRX53; + DEVELOPMENT_TEAM = UXA3H4X42S; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = InfiniLink/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; @@ -1071,7 +1071,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.2; - PRODUCT_BUNDLE_IDENTIFIER = com.titusk.InfiniLink; + PRODUCT_BUNDLE_IDENTIFIER = "com.alexemry.Infini-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/InfiniLink/InfiniLink.entitlements b/InfiniLink/InfiniLink.entitlements index cce046c..c5f2828 100644 --- a/InfiniLink/InfiniLink.entitlements +++ b/InfiniLink/InfiniLink.entitlements @@ -2,10 +2,28 @@ + aps-environment + development + com.apple.developer.healthkit + + com.apple.developer.healthkit.background-delivery + + com.apple.developer.icloud-container-identifiers + + iCloud.com.alexemry.Infini-iOS + + com.apple.developer.icloud-services + + CloudKit + + com.apple.developer.weatherkit + com.apple.security.app-sandbox com.apple.security.application-groups - + + group.com.alexemry.Infini-iOS + com.apple.security.device.bluetooth com.apple.security.files.user-selected.read-only diff --git a/InfiniLink/Info.plist b/InfiniLink/Info.plist index 82b78dd..22d3446 100644 --- a/InfiniLink/Info.plist +++ b/InfiniLink/Info.plist @@ -36,6 +36,10 @@ This app uses bluetooth to communicate with your PineTime. NSCalendarsUsageDescription This app needs access to your calendars to receive event reminders on your watch. + NSHealthShareUsageDescription + This app optionally uses Apple Health to write your heart rate and step count data. + NSHealthUpdateUsageDescription + This app optionally uses Apple Health to write your heart rate and step count data. NSLocationAlwaysAndWhenInUseUsageDescription This app needs your location to receive local weather information when the app is in the background. NSLocationAlwaysUsageDescription diff --git a/InfiniLink/Localizable.xcstrings b/InfiniLink/Localizable.xcstrings index 29e0b34..3ae3ebb 100644 --- a/InfiniLink/Localizable.xcstrings +++ b/InfiniLink/Localizable.xcstrings @@ -96,6 +96,16 @@ } } }, + "%@-%@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@-%2$@" + } + } + } + }, "%@:%@:%@" : { "localizations" : { "en" : { @@ -164,18 +174,18 @@ } } }, - "%lld-day" : { - - }, - "%lld–%lld " : { + "%lld-%lld " : { "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "%1$lld–%2$lld " + "value" : "%1$lld-%2$lld " } } } + }, + "%lld-day" : { + }, "%lld%%" : { @@ -795,9 +805,6 @@ } } } - }, - "Max" : { - }, "Maximum" : { "localizations" : { @@ -811,9 +818,6 @@ }, "Metric" : { - }, - "Min" : { - }, "Minimum" : { @@ -851,6 +855,9 @@ }, "No" : { + }, + "No Data" : { + }, "No Logs" : { diff --git a/InfiniLink/Utils/HealthKitManager.swift b/InfiniLink/Utils/HealthKitManager.swift index 9c2e70e..7a0e702 100644 --- a/InfiniLink/Utils/HealthKitManager.swift +++ b/InfiniLink/Utils/HealthKitManager.swift @@ -91,10 +91,10 @@ class HealthKitManager: ObservableObject { guard let healthStore = self.healthStore else { return } - /*healthStore.requestAuthorization(toShare: [stepsType, heartRateType, caloriesType, workoutType], read: [stepsType, heartRateType]) { success, error in + healthStore.requestAuthorization(toShare: [stepsType, heartRateType, caloriesType, workoutType], read: [stepsType, heartRateType]) { success, error in if let error = error { log(error.localizedDescription, caller: "HealthKitManager") } - }*/ + } } } From 204010f2e33150d98865638844f3c57e7c405a8b Mon Sep 17 00:00:00 2001 From: titus Date: Tue, 5 May 2026 16:21:35 +0200 Subject: [PATCH 6/6] fix padding cropping out left and right most bar --- InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift b/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift index f3746ea..beaf799 100644 --- a/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift +++ b/InfiniLink/Core/Components/Charts/Heart/HeartChartView.swift @@ -152,6 +152,7 @@ struct HeartChartView: View { } } .frame(height: 280) + .padding(.horizontal, 8) .chartYScale(domain: (displayedMin - 20)...(displayedMax + 20)) .chartXScale(domain: windowStart...windowEnd) .chartXAxis {