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