diff --git a/PulseHealth/PulseHealth.entitlements b/PulseHealth/PulseHealth.entitlements
index 2f178e1..51e2fec 100644
--- a/PulseHealth/PulseHealth.entitlements
+++ b/PulseHealth/PulseHealth.entitlements
@@ -6,9 +6,9 @@
com.apple.developer.healthkit.background-delivery
- keychain-access-groups
+ com.apple.security.application-groups
- $(AppIdentifierPrefix)com.daniil.pulsehealth.shared
+ group.com.daniil.pulsehealth
diff --git a/PulseHealth/Services/WidgetDataService.swift b/PulseHealth/Services/WidgetDataService.swift
new file mode 100644
index 0000000..6da9e1f
--- /dev/null
+++ b/PulseHealth/Services/WidgetDataService.swift
@@ -0,0 +1,25 @@
+import Foundation
+import WidgetKit
+
+enum WidgetDataService {
+ static let suiteName = "group.com.daniil.pulsehealth"
+
+ static var shared: UserDefaults? {
+ UserDefaults(suiteName: suiteName)
+ }
+
+ static func updateHabits(completed: Int, total: Int, tasksCount: Int) {
+ shared?.set(completed, forKey: "w_habits_completed")
+ shared?.set(total, forKey: "w_habits_total")
+ shared?.set(tasksCount, forKey: "w_tasks_count")
+ WidgetCenter.shared.reloadTimelines(ofKind: "HabitsProgress")
+ }
+
+ static func updateHealth(steps: Int, sleep: Double, heartRate: Int, readiness: Int) {
+ shared?.set(steps, forKey: "w_steps")
+ shared?.set(sleep, forKey: "w_sleep")
+ shared?.set(heartRate, forKey: "w_heart_rate")
+ shared?.set(readiness, forKey: "w_readiness")
+ WidgetCenter.shared.reloadTimelines(ofKind: "HealthSummary")
+ }
+}
diff --git a/PulseHealth/Views/Dashboard/DashboardView.swift b/PulseHealth/Views/Dashboard/DashboardView.swift
index d2ce136..669af76 100644
--- a/PulseHealth/Views/Dashboard/DashboardView.swift
+++ b/PulseHealth/Views/Dashboard/DashboardView.swift
@@ -203,6 +203,11 @@ struct DashboardView: View {
}
todayHabits = habits
isLoading = false
+
+ // Update widget
+ let completed = habits.filter { $0.completedToday == true }.count
+ let activeTasks = todayTasks.filter { !$0.completed }.count
+ WidgetDataService.updateHabits(completed: completed, total: habits.count, tasksCount: activeTasks)
}
func filterHabitsForToday(_ habits: [Habit]) -> [Habit] {
diff --git a/PulseHealth/Views/Health/HealthView.swift b/PulseHealth/Views/Health/HealthView.swift
index 6e24d28..8238c1a 100644
--- a/PulseHealth/Views/Health/HealthView.swift
+++ b/PulseHealth/Views/Health/HealthView.swift
@@ -200,6 +200,14 @@ struct HealthView: View {
async let h = HealthAPIService.shared.getHeatmap(days: 7)
readiness = try? await r; latest = try? await l; heatmapData = (try? await h) ?? []
isLoading = false
+
+ // Update widget
+ WidgetDataService.updateHealth(
+ steps: latest?.steps?.total ?? 0,
+ sleep: latest?.sleep?.totalSleep ?? 0,
+ heartRate: Int(latest?.restingHeartRate?.value ?? 0),
+ readiness: readiness?.score ?? 0
+ )
}
func syncHealthKit() async {
diff --git a/PulseWidget/HabitsProgressWidget.swift b/PulseWidget/HabitsProgressWidget.swift
index 7162911..899acc2 100644
--- a/PulseWidget/HabitsProgressWidget.swift
+++ b/PulseWidget/HabitsProgressWidget.swift
@@ -1,116 +1,41 @@
import WidgetKit
import SwiftUI
-// MARK: - Data
-
struct HabitsEntry: TimelineEntry {
let date: Date
let completed: Int
let total: Int
let tasksCount: Int
- let streakDays: Int
-
- var progress: Double {
- total > 0 ? Double(completed) / Double(total) : 0
- }
-
- static let placeholder = HabitsEntry(date: Date(), completed: 3, total: 5, tasksCount: 2, streakDays: 7)
+ var progress: Double { total > 0 ? Double(completed) / Double(total) : 0 }
}
-// MARK: - Provider
-
struct HabitsProvider: TimelineProvider {
- func placeholder(in context: Context) -> HabitsEntry { .placeholder }
+ func placeholder(in context: Context) -> HabitsEntry {
+ HabitsEntry(date: Date(), completed: 3, total: 5, tasksCount: 2)
+ }
func getSnapshot(in context: Context, completion: @escaping (HabitsEntry) -> Void) {
- completion(.placeholder)
+ completion(currentEntry())
}
func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
- Task {
- let entry = await fetchData()
- let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date()) ?? Date()
- completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
- }
+ let entry = currentEntry()
+ let next = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) ?? Date()
+ completion(Timeline(entries: [entry], policy: .after(next)))
}
- private func fetchData() async -> HabitsEntry {
- guard let token = KeychainService.load(key: KeychainService.tokenKey), !token.isEmpty else {
- return .placeholder
- }
-
- let baseURL = "https://api.digital-home.site"
-
- // Fetch habits
- var habits: [WidgetHabit] = []
- if let url = URL(string: "\(baseURL)/habits") {
- var req = URLRequest(url: url)
- req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
- req.timeoutInterval = 10
- if let (data, resp) = try? await URLSession.shared.data(for: req),
- (resp as? HTTPURLResponse)?.statusCode == 200 {
- habits = (try? JSONDecoder().decode([WidgetHabit].self, from: data)) ?? []
- }
- }
-
- // Fetch today's tasks
- var tasks: [WidgetTask] = []
- if let url = URL(string: "\(baseURL)/tasks/today") {
- var req = URLRequest(url: url)
- req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
- req.timeoutInterval = 10
- if let (data, resp) = try? await URLSession.shared.data(for: req),
- (resp as? HTTPURLResponse)?.statusCode == 200 {
- tasks = (try? JSONDecoder().decode([WidgetTask].self, from: data)) ?? []
- }
- }
-
- let activeHabits = habits.filter { !($0.isArchived ?? false) }
- let todayWeekday = Calendar.current.component(.weekday, from: Date()) - 1
- let todayHabits = activeHabits.filter { habit in
- guard habit.frequency == "weekly", let days = habit.targetDays, !days.isEmpty else { return true }
- return days.contains(todayWeekday)
- }
-
- // Check completed today (simplified — check completedToday from API or logs)
- let completed = todayHabits.filter { $0.completedToday ?? false }.count
- let activeTasks = tasks.filter { !$0.completed }.count
-
- return HabitsEntry(
+ private func currentEntry() -> HabitsEntry {
+ HabitsEntry(
date: Date(),
- completed: completed,
- total: todayHabits.count,
- tasksCount: activeTasks,
- streakDays: 0
+ completed: WidgetData.habitsCompleted,
+ total: WidgetData.habitsTotal,
+ tasksCount: WidgetData.tasksCount
)
}
}
-// Lightweight models for widget
-struct WidgetHabit: Codable {
- let id: Int
- let frequency: String
- let targetDays: [Int]?
- let isArchived: Bool?
- let completedToday: Bool?
- enum CodingKeys: String, CodingKey {
- case id, frequency
- case targetDays = "target_days"
- case isArchived = "is_archived"
- case completedToday = "completed_today"
- }
-}
-
-struct WidgetTask: Codable {
- let id: Int
- let completed: Bool
-}
-
-// MARK: - Views
-
struct HabitsProgressWidget: Widget {
let kind = "HabitsProgress"
-
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: HabitsProvider()) { entry in
HabitsWidgetView(entry: entry)
@@ -127,120 +52,65 @@ struct HabitsWidgetView: View {
@Environment(\.widgetFamily) var family
var body: some View {
- switch family {
- case .systemSmall: smallView
- case .systemMedium: mediumView
- default: smallView
+ Group {
+ if family == .systemMedium { mediumView } else { smallView }
}
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(Color(hex: "06060f"))
}
var smallView: some View {
VStack(spacing: 10) {
- // Ring
ZStack {
- Circle()
- .stroke(Color.white.opacity(0.1), lineWidth: 8)
- .frame(width: 64, height: 64)
- Circle()
- .trim(from: 0, to: entry.progress)
+ Circle().stroke(Color.white.opacity(0.1), lineWidth: 8).frame(width: 64, height: 64)
+ Circle().trim(from: 0, to: entry.progress)
.stroke(Color(hex: "0D9488"), style: StrokeStyle(lineWidth: 8, lineCap: .round))
- .frame(width: 64, height: 64)
- .rotationEffect(.degrees(-90))
+ .frame(width: 64, height: 64).rotationEffect(.degrees(-90))
VStack(spacing: 0) {
- Text("\(entry.completed)")
- .font(.system(size: 20, weight: .bold, design: .rounded))
- .foregroundColor(.white)
- Text("/\(entry.total)")
- .font(.system(size: 11))
- .foregroundColor(.gray)
+ Text("\(entry.completed)").font(.system(size: 20, weight: .bold, design: .rounded)).foregroundColor(.white)
+ Text("/\(entry.total)").font(.system(size: 11)).foregroundColor(.gray)
}
}
-
- Text("Привычки")
- .font(.caption2)
- .foregroundColor(.gray)
+ Text("Привычки").font(.caption2).foregroundColor(.gray)
}
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .background(Color(hex: "06060f"))
}
var mediumView: some View {
HStack(spacing: 16) {
- // Left: ring
ZStack {
- Circle()
- .stroke(Color.white.opacity(0.1), lineWidth: 8)
- .frame(width: 72, height: 72)
- Circle()
- .trim(from: 0, to: entry.progress)
+ Circle().stroke(Color.white.opacity(0.1), lineWidth: 8).frame(width: 72, height: 72)
+ Circle().trim(from: 0, to: entry.progress)
.stroke(Color(hex: "0D9488"), style: StrokeStyle(lineWidth: 8, lineCap: .round))
- .frame(width: 72, height: 72)
- .rotationEffect(.degrees(-90))
+ .frame(width: 72, height: 72).rotationEffect(.degrees(-90))
VStack(spacing: 0) {
- Text("\(entry.completed)")
- .font(.system(size: 22, weight: .bold, design: .rounded))
- .foregroundColor(.white)
- Text("/\(entry.total)")
- .font(.system(size: 12))
- .foregroundColor(.gray)
+ Text("\(entry.completed)").font(.system(size: 22, weight: .bold, design: .rounded)).foregroundColor(.white)
+ Text("/\(entry.total)").font(.system(size: 12)).foregroundColor(.gray)
}
}
-
- // Right: stats
VStack(alignment: .leading, spacing: 8) {
- Text("Прогресс дня")
- .font(.subheadline.bold())
- .foregroundColor(.white)
-
+ Text("Прогресс дня").font(.subheadline.bold()).foregroundColor(.white)
HStack(spacing: 12) {
- StatBadge(icon: "checkmark.circle.fill", value: "\(entry.completed)", label: "Готово", color: Color(hex: "0D9488"))
- StatBadge(icon: "calendar", value: "\(entry.tasksCount)", label: "Задач", color: Color(hex: "6366f1"))
+ Label("\(entry.completed) готово", systemImage: "checkmark.circle.fill").font(.caption2).foregroundColor(Color(hex: "0D9488"))
+ Label("\(entry.tasksCount) задач", systemImage: "calendar").font(.caption2).foregroundColor(Color(hex: "6366f1"))
}
-
- // Progress bar
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 3).fill(Color.white.opacity(0.1))
- RoundedRectangle(cornerRadius: 3)
- .fill(Color(hex: "0D9488"))
- .frame(width: geo.size.width * entry.progress)
+ RoundedRectangle(cornerRadius: 3).fill(Color(hex: "0D9488")).frame(width: geo.size.width * entry.progress)
}
- }
- .frame(height: 6)
+ }.frame(height: 6)
}
- }
- .padding(16)
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .background(Color(hex: "06060f"))
+ }.padding(16)
}
}
-struct StatBadge: View {
- let icon: String
- let value: String
- let label: String
- let color: Color
-
- var body: some View {
- HStack(spacing: 4) {
- Image(systemName: icon).font(.caption2).foregroundColor(color)
- Text(value).font(.caption.bold()).foregroundColor(.white)
- Text(label).font(.caption2).foregroundColor(.gray)
- }
- }
-}
-
-// Color extension for widget
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
- var int: UInt64 = 0
- Scanner(string: hex).scanHexInt64(&int)
+ var int: UInt64 = 0; Scanner(string: hex).scanHexInt64(&int)
let r, g, b: UInt64
- switch hex.count {
- case 6: (r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF)
- default: (r, g, b) = (0, 0, 0)
- }
+ if hex.count == 6 { (r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF) }
+ else { (r, g, b) = (0, 0, 0) }
self.init(.sRGB, red: Double(r)/255, green: Double(g)/255, blue: Double(b)/255)
}
}
diff --git a/PulseWidget/HealthSummaryWidget.swift b/PulseWidget/HealthSummaryWidget.swift
index f6a394f..61eaae6 100644
--- a/PulseWidget/HealthSummaryWidget.swift
+++ b/PulseWidget/HealthSummaryWidget.swift
@@ -1,90 +1,42 @@
import WidgetKit
import SwiftUI
-// MARK: - Data
-
struct HealthEntry: TimelineEntry {
let date: Date
let steps: Int
let sleep: Double
let heartRate: Int
let readinessScore: Int
-
- static let placeholder = HealthEntry(date: Date(), steps: 6234, sleep: 7.5, heartRate: 68, readinessScore: 80)
}
-// MARK: - Provider
-
struct HealthProvider: TimelineProvider {
- func placeholder(in context: Context) -> HealthEntry { .placeholder }
+ func placeholder(in context: Context) -> HealthEntry {
+ HealthEntry(date: Date(), steps: 6234, sleep: 7.5, heartRate: 68, readinessScore: 80)
+ }
func getSnapshot(in context: Context, completion: @escaping (HealthEntry) -> Void) {
- completion(.placeholder)
+ completion(currentEntry())
}
func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
- Task {
- let entry = await fetchHealthData()
- let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date()) ?? Date()
- completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
- }
+ let entry = currentEntry()
+ let next = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) ?? Date()
+ completion(Timeline(entries: [entry], policy: .after(next)))
}
- private func fetchHealthData() async -> HealthEntry {
- let baseURL = "https://health.digital-home.site"
- guard let token = KeychainService.load(key: KeychainService.healthTokenKey) else {
- return .placeholder
- }
-
- // Fetch latest
- var steps = 0, sleep = 0.0, hr = 0
-
- if let url = URL(string: "\(baseURL)/api/health/latest") {
- var req = URLRequest(url: url)
- req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
- req.timeoutInterval = 10
- if let (data, resp) = try? await URLSession.shared.data(for: req),
- (resp as? HTTPURLResponse)?.statusCode == 200,
- let json = try? JSONDecoder().decode(WidgetHealthLatest.self, from: data) {
- steps = json.steps?.total ?? 0
- sleep = json.sleep?.totalSleep ?? 0
- hr = Int(json.restingHeartRate?.value ?? 0)
- }
- }
-
- // Fetch readiness
- var score = 0
- if let url = URL(string: "\(baseURL)/api/health/readiness") {
- var req = URLRequest(url: url)
- req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
- req.timeoutInterval = 10
- if let (data, resp) = try? await URLSession.shared.data(for: req),
- (resp as? HTTPURLResponse)?.statusCode == 200,
- let json = try? JSONDecoder().decode(WidgetReadiness.self, from: data) {
- score = json.score
- }
- }
-
- return HealthEntry(date: Date(), steps: steps, sleep: sleep, heartRate: hr, readinessScore: score)
+ private func currentEntry() -> HealthEntry {
+ HealthEntry(
+ date: Date(),
+ steps: WidgetData.steps,
+ sleep: WidgetData.sleep,
+ heartRate: WidgetData.heartRate,
+ readinessScore: WidgetData.readinessScore
+ )
}
}
-// Lightweight models
-struct WidgetHealthLatest: Codable {
- let steps: WidgetSteps?
- let sleep: WidgetSleep?
- let restingHeartRate: WidgetRHR?
-}
-struct WidgetSteps: Codable { let total: Int? }
-struct WidgetSleep: Codable { let totalSleep: Double? }
-struct WidgetRHR: Codable { let value: Double? }
-struct WidgetReadiness: Codable { let score: Int }
-
-// MARK: - Widget
-
struct HealthSummaryWidget: Widget {
let kind = "HealthSummary"
-
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: HealthProvider()) { entry in
HealthWidgetView(entry: entry)
@@ -96,29 +48,26 @@ struct HealthSummaryWidget: Widget {
}
}
-// MARK: - Views
-
struct HealthWidgetView: View {
let entry: HealthEntry
@Environment(\.widgetFamily) var family
- var body: some View {
- switch family {
- case .systemSmall: smallView
- case .systemMedium: mediumView
- default: smallView
- }
- }
-
var readinessColor: Color {
if entry.readinessScore >= 80 { return Color(hex: "0D9488") }
if entry.readinessScore >= 60 { return Color(hex: "ffa502") }
return Color(hex: "ff4757")
}
+ var body: some View {
+ Group {
+ if family == .systemMedium { mediumView } else { smallView }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(Color(hex: "06060f"))
+ }
+
var smallView: some View {
VStack(spacing: 8) {
- // Readiness
ZStack {
Circle().stroke(Color.white.opacity(0.1), lineWidth: 6).frame(width: 50, height: 50)
Circle().trim(from: 0, to: CGFloat(entry.readinessScore) / 100)
@@ -126,7 +75,6 @@ struct HealthWidgetView: View {
.frame(width: 50, height: 50).rotationEffect(.degrees(-90))
Text("\(entry.readinessScore)").font(.system(size: 16, weight: .bold, design: .rounded)).foregroundColor(readinessColor)
}
-
VStack(spacing: 4) {
HStack(spacing: 4) {
Image(systemName: "figure.walk").font(.system(size: 9)).foregroundColor(Color(hex: "ffa502"))
@@ -138,13 +86,10 @@ struct HealthWidgetView: View {
}
}
}
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .background(Color(hex: "06060f"))
}
var mediumView: some View {
HStack(spacing: 16) {
- // Readiness ring
ZStack {
Circle().stroke(Color.white.opacity(0.1), lineWidth: 8).frame(width: 68, height: 68)
Circle().trim(from: 0, to: CGFloat(entry.readinessScore) / 100)
@@ -155,32 +100,23 @@ struct HealthWidgetView: View {
Text("балл").font(.system(size: 9)).foregroundColor(.gray)
}
}
-
- // Metrics
VStack(alignment: .leading, spacing: 6) {
Text("Здоровье").font(.subheadline.bold()).foregroundColor(.white)
HStack(spacing: 14) {
- HealthBadge(icon: "figure.walk", value: "\(entry.steps)", color: Color(hex: "ffa502"))
- HealthBadge(icon: "moon.fill", value: String(format: "%.1fч", entry.sleep), color: Color(hex: "7c3aed"))
- HealthBadge(icon: "heart.fill", value: "\(entry.heartRate)", color: Color(hex: "ff4757"))
+ VStack(spacing: 3) {
+ Image(systemName: "figure.walk").font(.caption).foregroundColor(Color(hex: "ffa502"))
+ Text("\(entry.steps)").font(.caption2.bold()).foregroundColor(.white)
+ }
+ VStack(spacing: 3) {
+ Image(systemName: "moon.fill").font(.caption).foregroundColor(Color(hex: "7c3aed"))
+ Text(String(format: "%.1fч", entry.sleep)).font(.caption2.bold()).foregroundColor(.white)
+ }
+ VStack(spacing: 3) {
+ Image(systemName: "heart.fill").font(.caption).foregroundColor(Color(hex: "ff4757"))
+ Text("\(entry.heartRate)").font(.caption2.bold()).foregroundColor(.white)
+ }
}
}
- }
- .padding(16)
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .background(Color(hex: "06060f"))
- }
-}
-
-struct HealthBadge: View {
- let icon: String
- let value: String
- let color: Color
-
- var body: some View {
- VStack(spacing: 3) {
- Image(systemName: icon).font(.caption).foregroundColor(color)
- Text(value).font(.caption2.bold()).foregroundColor(.white)
- }
+ }.padding(16)
}
}
diff --git a/PulseWidget/Info.plist b/PulseWidget/Info.plist
new file mode 100644
index 0000000..077e0ca
--- /dev/null
+++ b/PulseWidget/Info.plist
@@ -0,0 +1,29 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Pulse Widget
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ XPC!
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+
+
diff --git a/PulseWidget/KeychainService.swift b/PulseWidget/KeychainService.swift
index 782fa28..fb68187 100644
--- a/PulseWidget/KeychainService.swift
+++ b/PulseWidget/KeychainService.swift
@@ -1,49 +1,18 @@
import Foundation
-import Security
-enum KeychainService {
- static let service = "com.daniil.pulsehealth"
+// Widget reads data from shared UserDefaults (App Group), not Keychain
+enum WidgetData {
+ static let suiteName = "group.com.daniil.pulsehealth"
- static func save(key: String, value: String) {
- guard let data = value.data(using: .utf8) else { return }
- let query: [String: Any] = [
- kSecClass as String: kSecClassGenericPassword,
- kSecAttrService as String: service,
- kSecAttrAccount as String: key
- ]
- SecItemDelete(query as CFDictionary)
- var add = query
- add[kSecValueData as String] = data
- add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
- SecItemAdd(add as CFDictionary, nil)
+ static var shared: UserDefaults? {
+ UserDefaults(suiteName: suiteName)
}
- static func load(key: String) -> String? {
- let query: [String: Any] = [
- kSecClass as String: kSecClassGenericPassword,
- kSecAttrService as String: service,
- kSecAttrAccount as String: key,
- kSecReturnData as String: true,
- kSecMatchLimit as String: kSecMatchLimitOne
- ]
- var result: AnyObject?
- guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
- let data = result as? Data else { return nil }
- return String(data: data, encoding: .utf8)
- }
-
- static func delete(key: String) {
- let query: [String: Any] = [
- kSecClass as String: kSecClassGenericPassword,
- kSecAttrService as String: service,
- kSecAttrAccount as String: key
- ]
- SecItemDelete(query as CFDictionary)
- }
-
- // Keys
- static let tokenKey = "auth_token"
- static let refreshTokenKey = "auth_refresh_token"
- static let healthTokenKey = "health_jwt_token"
- static let healthApiKeyKey = "health_api_key"
+ static var habitsCompleted: Int { shared?.integer(forKey: "w_habits_completed") ?? 0 }
+ static var habitsTotal: Int { shared?.integer(forKey: "w_habits_total") ?? 0 }
+ static var tasksCount: Int { shared?.integer(forKey: "w_tasks_count") ?? 0 }
+ static var steps: Int { shared?.integer(forKey: "w_steps") ?? 0 }
+ static var sleep: Double { shared?.double(forKey: "w_sleep") ?? 0 }
+ static var heartRate: Int { shared?.integer(forKey: "w_heart_rate") ?? 0 }
+ static var readinessScore: Int { shared?.integer(forKey: "w_readiness") ?? 0 }
}
diff --git a/PulseWidget/PulseWidget.entitlements b/PulseWidget/PulseWidget.entitlements
index 5b8fed1..91b544c 100644
--- a/PulseWidget/PulseWidget.entitlements
+++ b/PulseWidget/PulseWidget.entitlements
@@ -2,9 +2,9 @@
- keychain-access-groups
+ com.apple.security.application-groups
- $(AppIdentifierPrefix)com.daniil.pulsehealth.shared
+ group.com.daniil.pulsehealth
diff --git a/project.yml b/project.yml
index ab997d4..850cf4a 100644
--- a/project.yml
+++ b/project.yml
@@ -25,11 +25,16 @@ targets:
BackgroundModes:
modes:
- processing
+ AppGroups:
+ groups:
+ - group.com.daniil.pulsehealth
PulseWidgetExtension:
type: app-extension
platform: iOS
sources: PulseWidget
+ entitlements:
+ path: PulseWidget/PulseWidget.entitlements
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.daniil.pulsehealth.widget
@@ -40,3 +45,7 @@ targets:
MARKETING_VERSION: "1.0"
CURRENT_PROJECT_VERSION: "1"
INFOPLIST_KEY_CFBundleDisplayName: Pulse Widget
+ capabilities:
+ AppGroups:
+ groups:
+ - group.com.daniil.pulsehealth