b953908f58
Complete implementation of a SwiftUI iOS app that serves as a "Single Source of Truth" for health data. The app reads from all Apple Health sources, detects conflicts between devices, merges data using configurable strategies, and writes cleaned data back. Features: - Phase 1: HealthKit integration with automatic source discovery - Phase 2: DataReader with conflict detection (time-window based) - Phase 3: RuleEngine with 8 merge strategies (exclusive, priority, higher wins, etc.) - Phase 4: MergeEngine for conflict resolution + DataWriter for HealthKit writes - Phase 5: SwiftUI UI for dashboard, conflicts, rules, and sources management - Phase 6: Background sync with configurable intervals and push notifications - Phase 7: Complete rule editor and polished UI components Supported data types: - Steps, Heart Rate, Blood Pressure, SpO2, Sleep - Distance, Floors Climbed, Active Energy, HRV, Respiratory Rate Architecture: SourceManager -> DataReader -> RuleEngine -> MergeEngine -> DataWriter
154 lines
3.8 KiB
Swift
154 lines
3.8 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
// MARK: - Date Extensions
|
|
extension Date {
|
|
var startOfDay: Date {
|
|
Calendar.current.startOfDay(for: self)
|
|
}
|
|
|
|
var endOfDay: Date {
|
|
Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)!.addingTimeInterval(-1)
|
|
}
|
|
|
|
var isToday: Bool {
|
|
Calendar.current.isDateInToday(self)
|
|
}
|
|
|
|
var isYesterday: Bool {
|
|
Calendar.current.isDateInYesterday(self)
|
|
}
|
|
|
|
func formatted(style: DateFormatter.Style) -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = style
|
|
formatter.timeStyle = .none
|
|
return formatter.string(from: self)
|
|
}
|
|
|
|
func formattedTime() -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .none
|
|
formatter.timeStyle = .short
|
|
return formatter.string(from: self)
|
|
}
|
|
|
|
func formattedRelative() -> String {
|
|
let formatter = RelativeDateTimeFormatter()
|
|
formatter.unitsStyle = .abbreviated
|
|
return formatter.localizedString(for: self, relativeTo: Date())
|
|
}
|
|
|
|
func adding(days: Int) -> Date {
|
|
Calendar.current.date(byAdding: .day, value: days, to: self)!
|
|
}
|
|
|
|
func adding(hours: Int) -> Date {
|
|
Calendar.current.date(byAdding: .hour, value: hours, to: self)!
|
|
}
|
|
|
|
func adding(minutes: Int) -> Date {
|
|
Calendar.current.date(byAdding: .minute, value: minutes, to: self)!
|
|
}
|
|
}
|
|
|
|
// MARK: - Double Extensions
|
|
extension Double {
|
|
func formatted(decimals: Int = 1) -> String {
|
|
String(format: "%.\(decimals)f", self)
|
|
}
|
|
|
|
var formattedAsInteger: String {
|
|
String(format: "%.0f", self)
|
|
}
|
|
|
|
var formattedAsPercentage: String {
|
|
String(format: "%.1f%%", self * 100)
|
|
}
|
|
}
|
|
|
|
// MARK: - Array Extensions
|
|
extension Array {
|
|
func chunked(into size: Int) -> [[Element]] {
|
|
stride(from: 0, to: count, by: size).map {
|
|
Array(self[$0..<Swift.min($0 + size, count)])
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Color Extensions
|
|
extension Color {
|
|
static let healthBridgePrimary = Color.blue
|
|
static let healthBridgeSecondary = Color.cyan
|
|
static let healthBridgeAccent = Color.orange
|
|
|
|
static func forSeverity(_ severity: ConflictSeverity) -> Color {
|
|
switch severity {
|
|
case .minor: return .green
|
|
case .moderate: return .yellow
|
|
case .significant: return .orange
|
|
case .major: return .red
|
|
}
|
|
}
|
|
|
|
static func forQuality(_ quality: DataQuality) -> Color {
|
|
switch quality {
|
|
case .complete: return .green
|
|
case .partial: return .yellow
|
|
case .missing: return .gray
|
|
case .invalid: return .red
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - View Extensions
|
|
extension View {
|
|
func cardStyle() -> some View {
|
|
self
|
|
.padding()
|
|
.background(Color(.systemBackground))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.shadow(color: .black.opacity(0.05), radius: 4, y: 2)
|
|
}
|
|
|
|
func sectionHeader(_ title: String) -> some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text(title)
|
|
.font(.headline)
|
|
.foregroundStyle(.primary)
|
|
self
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Binding Extensions
|
|
extension Binding {
|
|
func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
|
|
Binding(
|
|
get: { self.wrappedValue },
|
|
set: { newValue in
|
|
self.wrappedValue = newValue
|
|
handler(newValue)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Optional Extensions
|
|
extension Optional where Wrapped == String {
|
|
var orEmpty: String {
|
|
self ?? ""
|
|
}
|
|
|
|
var isNilOrEmpty: Bool {
|
|
self?.isEmpty ?? true
|
|
}
|
|
}
|
|
|
|
// MARK: - Collection Extensions
|
|
extension Collection {
|
|
var isNotEmpty: Bool {
|
|
!isEmpty
|
|
}
|
|
}
|