diff --git a/PulseHealth/PulseHealth.entitlements b/PulseHealth/PulseHealth.entitlements
index 54bc426..2f178e1 100644
--- a/PulseHealth/PulseHealth.entitlements
+++ b/PulseHealth/PulseHealth.entitlements
@@ -6,5 +6,9 @@
com.apple.developer.healthkit.background-delivery
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)com.daniil.pulsehealth.shared
+
diff --git a/PulseHealth/Services/HealthKitService.swift b/PulseHealth/Services/HealthKitService.swift
index 5f438c0..c746454 100644
--- a/PulseHealth/Services/HealthKitService.swift
+++ b/PulseHealth/Services/HealthKitService.swift
@@ -154,10 +154,16 @@ class HealthKitService: ObservableObject {
private func fetchSleepData(dateFormatter: DateFormatter) async -> [[String: Any]] {
guard let sleepType = HKCategoryType.categoryType(forIdentifier: .sleepAnalysis) else { return [] }
- // Берём последние 24 часа, чтобы захватить ночной сон
+ // Ночной сон: с 18:00 вчера до сейчас (захватывает засыпание вечером + пробуждение утром)
let now = Date()
- guard let yesterday = Calendar.current.date(byAdding: .hour, value: -24, to: now) else { return [] }
- let sleepPredicate = HKQuery.predicateForSamples(withStart: yesterday, end: now)
+ let cal = Calendar.current
+ var startComponents = cal.dateComponents([.year, .month, .day], from: now)
+ startComponents.hour = 18
+ startComponents.minute = 0
+ guard let todayEvening = cal.date(from: startComponents),
+ let yesterdayEvening = cal.date(byAdding: .day, value: -1, to: todayEvening) else { return [] }
+ let sleepStart = now.timeIntervalSince(todayEvening) > 0 ? todayEvening : yesterdayEvening
+ let sleepPredicate = HKQuery.predicateForSamples(withStart: sleepStart, end: now)
return await withCheckedContinuation { cont in
let sort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
@@ -328,8 +334,13 @@ class HealthKitService: ObservableObject {
func fetchSleepSegments() async -> [SleepSegment] {
guard let sleepType = HKCategoryType.categoryType(forIdentifier: .sleepAnalysis) else { return [] }
let now = Date()
- guard let yesterday = Calendar.current.date(byAdding: .hour, value: -24, to: now) else { return [] }
- let predicate = HKQuery.predicateForSamples(withStart: yesterday, end: now)
+ let cal = Calendar.current
+ var startComp = cal.dateComponents([.year, .month, .day], from: now)
+ startComp.hour = 18
+ guard let todayEvening = cal.date(from: startComp),
+ let yesterdayEvening = cal.date(byAdding: .day, value: -1, to: todayEvening) else { return [] }
+ let sleepStart = now.timeIntervalSince(todayEvening) > 0 ? todayEvening : yesterdayEvening
+ let predicate = HKQuery.predicateForSamples(withStart: sleepStart, end: now)
return await withCheckedContinuation { cont in
let sort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
diff --git a/PulseHealth/Services/KeychainService.swift b/PulseHealth/Services/KeychainService.swift
index 782fa28..434763f 100644
--- a/PulseHealth/Services/KeychainService.swift
+++ b/PulseHealth/Services/KeychainService.swift
@@ -3,13 +3,15 @@ import Security
enum KeychainService {
static let service = "com.daniil.pulsehealth"
+ static let accessGroup = "V9AG8JTFLC.com.daniil.pulsehealth.shared"
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
+ kSecAttrAccount as String: key,
+ kSecAttrAccessGroup as String: accessGroup
]
SecItemDelete(query as CFDictionary)
var add = query
@@ -23,6 +25,7 @@ enum KeychainService {
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
+ kSecAttrAccessGroup as String: accessGroup,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
@@ -36,7 +39,8 @@ enum KeychainService {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
- kSecAttrAccount as String: key
+ kSecAttrAccount as String: key,
+ kSecAttrAccessGroup as String: accessGroup
]
SecItemDelete(query as CFDictionary)
}
diff --git a/PulseWidget/HabitsProgressWidget.swift b/PulseWidget/HabitsProgressWidget.swift
new file mode 100644
index 0000000..7162911
--- /dev/null
+++ b/PulseWidget/HabitsProgressWidget.swift
@@ -0,0 +1,246 @@
+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)
+}
+
+// MARK: - Provider
+
+struct HabitsProvider: TimelineProvider {
+ func placeholder(in context: Context) -> HabitsEntry { .placeholder }
+
+ func getSnapshot(in context: Context, completion: @escaping (HabitsEntry) -> Void) {
+ completion(.placeholder)
+ }
+
+ 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)))
+ }
+ }
+
+ 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(
+ date: Date(),
+ completed: completed,
+ total: todayHabits.count,
+ tasksCount: activeTasks,
+ streakDays: 0
+ )
+ }
+}
+
+// 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)
+ .containerBackground(.fill.tertiary, for: .widget)
+ }
+ .configurationDisplayName("Прогресс дня")
+ .description("Привычки и задачи на сегодня")
+ .supportedFamilies([.systemSmall, .systemMedium])
+ }
+}
+
+struct HabitsWidgetView: View {
+ let entry: HabitsEntry
+ @Environment(\.widgetFamily) var family
+
+ var body: some View {
+ switch family {
+ case .systemSmall: smallView
+ case .systemMedium: mediumView
+ default: smallView
+ }
+ }
+
+ 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)
+ .stroke(Color(hex: "0D9488"), style: StrokeStyle(lineWidth: 8, lineCap: .round))
+ .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("Привычки")
+ .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)
+ .stroke(Color(hex: "0D9488"), style: StrokeStyle(lineWidth: 8, lineCap: .round))
+ .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)
+ }
+ }
+
+ // Right: stats
+ VStack(alignment: .leading, spacing: 8) {
+ 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"))
+ }
+
+ // 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)
+ }
+ }
+ .frame(height: 6)
+ }
+ }
+ .padding(16)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(Color(hex: "06060f"))
+ }
+}
+
+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)
+ 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)
+ }
+ 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
new file mode 100644
index 0000000..f6a394f
--- /dev/null
+++ b/PulseWidget/HealthSummaryWidget.swift
@@ -0,0 +1,186 @@
+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 getSnapshot(in context: Context, completion: @escaping (HealthEntry) -> Void) {
+ completion(.placeholder)
+ }
+
+ 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)))
+ }
+ }
+
+ 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)
+ }
+}
+
+// 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)
+ .containerBackground(.fill.tertiary, for: .widget)
+ }
+ .configurationDisplayName("Здоровье")
+ .description("Шаги, сон, пульс и готовность")
+ .supportedFamilies([.systemSmall, .systemMedium])
+ }
+}
+
+// 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 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)
+ .stroke(readinessColor, style: StrokeStyle(lineWidth: 6, lineCap: .round))
+ .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"))
+ Text("\(entry.steps)").font(.caption2.bold()).foregroundColor(.white)
+ }
+ HStack(spacing: 4) {
+ Image(systemName: "moon.fill").font(.system(size: 9)).foregroundColor(Color(hex: "7c3aed"))
+ Text(String(format: "%.1fч", entry.sleep)).font(.caption2.bold()).foregroundColor(.white)
+ }
+ }
+ }
+ .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)
+ .stroke(readinessColor, style: StrokeStyle(lineWidth: 8, lineCap: .round))
+ .frame(width: 68, height: 68).rotationEffect(.degrees(-90))
+ VStack(spacing: 0) {
+ Text("\(entry.readinessScore)").font(.system(size: 20, weight: .bold, design: .rounded)).foregroundColor(readinessColor)
+ 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"))
+ }
+ }
+ }
+ .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)
+ }
+ }
+}
diff --git a/PulseWidget/KeychainService.swift b/PulseWidget/KeychainService.swift
new file mode 100644
index 0000000..434763f
--- /dev/null
+++ b/PulseWidget/KeychainService.swift
@@ -0,0 +1,53 @@
+import Foundation
+import Security
+
+enum KeychainService {
+ static let service = "com.daniil.pulsehealth"
+ static let accessGroup = "V9AG8JTFLC.com.daniil.pulsehealth.shared"
+
+ 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,
+ kSecAttrAccessGroup as String: accessGroup
+ ]
+ SecItemDelete(query as CFDictionary)
+ var add = query
+ add[kSecValueData as String] = data
+ add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
+ SecItemAdd(add as CFDictionary, nil)
+ }
+
+ static func load(key: String) -> String? {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: service,
+ kSecAttrAccount as String: key,
+ kSecAttrAccessGroup as String: accessGroup,
+ 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,
+ kSecAttrAccessGroup as String: accessGroup
+ ]
+ 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"
+}
diff --git a/PulseWidget/PulseWidget.entitlements b/PulseWidget/PulseWidget.entitlements
new file mode 100644
index 0000000..5b8fed1
--- /dev/null
+++ b/PulseWidget/PulseWidget.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)com.daniil.pulsehealth.shared
+
+
+
diff --git a/PulseWidget/PulseWidgetBundle.swift b/PulseWidget/PulseWidgetBundle.swift
new file mode 100644
index 0000000..8a87e7e
--- /dev/null
+++ b/PulseWidget/PulseWidgetBundle.swift
@@ -0,0 +1,10 @@
+import WidgetKit
+import SwiftUI
+
+@main
+struct PulseWidgetBundle: WidgetBundle {
+ var body: some Widget {
+ HabitsProgressWidget()
+ HealthSummaryWidget()
+ }
+}
diff --git a/project.yml b/project.yml
index 89d497a..2d34d37 100644
--- a/project.yml
+++ b/project.yml
@@ -10,6 +10,8 @@ targets:
sources: PulseHealth
entitlements:
path: PulseHealth/PulseHealth.entitlements
+ dependencies:
+ - target: PulseWidgetExtension
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.daniil.pulsehealth
@@ -23,3 +25,23 @@ targets:
BackgroundModes:
modes:
- processing
+
+ PulseWidgetExtension:
+ type: app-extension
+ platform: iOS
+ sources: PulseWidget
+ entitlements:
+ path: PulseWidget/PulseWidget.entitlements
+ settings:
+ base:
+ PRODUCT_BUNDLE_IDENTIFIER: com.daniil.pulsehealth.widget
+ SWIFT_VERSION: 5.9
+ CODE_SIGN_STYLE: Automatic
+ DEVELOPMENT_TEAM: V9AG8JTFLC
+ INFOPLIST_FILE: ""
+ info:
+ path: PulseWidget/Info.plist
+ properties:
+ CFBundleDisplayName: Pulse Widget
+ NSExtension:
+ NSExtensionPointIdentifier: com.apple.widgetkit-extension