diff --git a/examples/ios-widget-steps/README.md b/examples/ios-widget-steps/README.md new file mode 100644 index 0000000000..ab6def3cf3 --- /dev/null +++ b/examples/ios-widget-steps/README.md @@ -0,0 +1,123 @@ +# iOS 26 – Schritte Widget (Apple Health) + +Ein vollständiges, importierbares Xcode-Projekt mit einer iOS-App und einem WidgetKit-Extension, das täglich Schritt-Daten direkt aus Apple Health liest. + +## Vorschau + +| Small | Medium | Lock Screen (Circular) | Lock Screen (Rectangular) | +|-------|--------|------------------------|---------------------------| +| Fortschrittsring + Schrittzahl | Ring + Distanz, Ziel, Fortschrittsbalken | Gauge-Ring mit kompakter Zahl | Schrittzahl + Fortschrittsbalken | + +## Funktionen + +- **Echtzeitdaten** aus Apple Health (HealthKit) +- **Fortschrittsring** – animiert, wird grün bei Zielerreichung +- **Distanzberechnung** (km) und **Kalorienabschätzung** (kcal) +- **Anpassbares Tagesziel** (Standard: 10.000) – wird im App-Group-UserDefaults gespeichert +- **Automatische Widget-Aktualisierung** alle 15 Minuten +- **5 Widget-Größen:** Small, Medium, Accessory Circular, Rectangular, Inline (Lock Screen) +- Vollständige **Xcode 16 Previews** mit `#Preview(as:)` Makro + +## Anforderungen + +| | | +|---|---| +| iOS | 26.0+ | +| Xcode | 16.0+ | +| Swift | 6.0 | +| Frameworks | HealthKit, WidgetKit, SwiftUI | + +## Projekt öffnen + +```bash +open examples/ios-widget-steps/StepsWidget/StepsWidget.xcodeproj +``` + +## Projektstruktur + +``` +StepsWidget/ +├── StepsWidget.xcodeproj/ +│ └── project.pbxproj +│ +├── StepsApp/ # Hauptapp-Target +│ ├── StepsApp.swift # @main App +│ ├── ContentView.swift # Fortschrittsring, Stats, Ziel-Editor +│ ├── HealthKitManager.swift # HealthKit-Abfragen + WidgetCenter reload +│ ├── Info.plist # NSHealthShareUsageDescription +│ ├── StepsApp.entitlements # HealthKit + App Group +│ └── Assets.xcassets/ +│ +└── StepsWidgetExtension/ # Widget-Extension-Target + ├── StepsWidgetBundle.swift # @main WidgetBundle + ├── StepsProvider.swift # TimelineProvider + StepsEntry + ├── StepsWidgetView.swift # Alle Widget-Views + Widget-Config + ├── Info.plist # NSExtensionPointIdentifier + ├── StepsWidgetExtension.entitlements # HealthKit + App Group + └── Assets.xcassets/ +``` + +## Setup nach dem Öffnen in Xcode + +### 1. Team & Bundle ID setzen + +Wähle in Xcode beide Targets (`StepsApp` und `StepsWidgetExtension`) → **Signing & Capabilities** → wähle dein Team. + +Die Bundle-IDs sind vorbelegt als: +- App: `com.example.stepswidget` +- Widget: `com.example.stepswidget.widget` + +Passe sie auf deine eigene Domain an, z. B. `de.deinname.stepswidget`. + +### 2. App Group konfigurieren + +Beide Targets müssen zur **gleichen App Group** gehören, damit das Widget das Tagesziel aus der App lesen kann: + +1. Target `StepsApp` → **Signing & Capabilities** → **+ Capability** → *App Groups* +2. Gruppe hinzufügen: `group.com.example.stepswidget` + *(oder deine eigene: `group.de.deinname.stepswidget`)* +3. Dasselbe für `StepsWidgetExtension` wiederholen +4. Den App-Group-Identifier in `HealthKitManager.swift` und `StepsProvider.swift` anpassen: + ```swift + private static let appGroup = "group.com.example.stepswidget" + ``` + +### 3. HealthKit-Berechtigung + +Die App fragt beim ersten Start automatisch nach HealthKit-Zugriff. +Das Widget fordert die Berechtigung ebenfalls beim ersten Aktualisieren an. + +> **Hinweis:** Der iOS-Simulator enthält **keine** HealthKit-Daten. +> Zum Testen ein **echtes Gerät** verwenden oder die Simulator-Health-App zum Eintragen von Schritten nutzen. + +## Architektur + +``` +┌─────────────────────────────────┐ App Group UserDefaults +│ StepsApp │ ───────────────────────────────┐ +│ HealthKitManager (@MainActor) │ schreibt stepGoal │ +│ ├─ requestAuthorization() │ │ +│ ├─ fetchTodaySteps() async │ ▼ +│ └─ updateGoal(_:) → WidgetCenter.reloadTimelines() ┌──────────────────────┐ +└─────────────────────────────────┘ │ StepsWidgetExtension │ + │ StepsProvider │ +HealthKit Store │ ├─ liest stepGoal │ + │ HKStatisticsQuery (stepCount) │ └─ HKStatisticsQuery │ + └──────────────────────────────────────────────────┤ │ + │ StepsWidget (Views) │ + └──────────────────────┘ +``` + +## Widget-Aktualisierung + +Das Widget aktualisiert sich automatisch alle **15 Minuten** (WidgetKit policy). +Wenn der Nutzer das Tagesziel in der App ändert, wird das Widget sofort neu geladen: + +```swift +WidgetCenter.shared.reloadTimelines(ofKind: "StepsWidget") +``` + +## Lokalisierung + +Alle Texte sind auf Deutsch. Für andere Sprachen können die Strings in eine +`Localizable.xcstrings`-Datei extrahiert werden (`SWIFT_EMIT_LOC_STRINGS = YES` ist gesetzt). diff --git a/examples/ios-widget-steps/StepsWidget/StepsApp/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/ios-widget-steps/StepsWidget/StepsApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..805e83152a --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.478", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.478", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/ios-widget-steps/StepsWidget/StepsApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..13613e3ee1 --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsApp/Assets.xcassets/Contents.json b/examples/ios-widget-steps/StepsWidget/StepsApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsApp/ContentView.swift b/examples/ios-widget-steps/StepsWidget/StepsApp/ContentView.swift new file mode 100644 index 0000000000..a0c817fb3b --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsApp/ContentView.swift @@ -0,0 +1,182 @@ +import SwiftUI + +struct ContentView: View { + @StateObject private var manager = HealthKitManager() + @State private var showGoalSheet = false + @State private var goalDraft: Double = 10_000 + + private var ringColor: Color { manager.progress >= 1.0 ? .green : .blue } + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 28) { + progressRing + statsGrid + goalCard + if manager.authorizationStatus == .denied { deniedBanner } + } + .padding(.bottom, 24) + } + .navigationTitle("Schritte") + .navigationBarTitleDisplayMode(.large) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + Task { await manager.fetchTodaySteps() } + } label: { + Image(systemName: "arrow.clockwise") + } + } + } + .sheet(isPresented: $showGoalSheet) { + GoalEditorSheet(goal: $goalDraft) { manager.updateGoal(Int(goalDraft)) } + } + .task { manager.requestAuthorization() } + } + } + + // MARK: - Sub-views + + private var progressRing: some View { + ZStack { + Circle() + .stroke(ringColor.opacity(0.15), lineWidth: 22) + Circle() + .trim(from: 0, to: manager.progress) + .stroke(ringColor, style: StrokeStyle(lineWidth: 22, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .animation(.easeInOut(duration: 0.8), value: manager.progress) + VStack(spacing: 4) { + Text(manager.todaySteps.formatted()) + .font(.system(size: 52, weight: .bold, design: .rounded)) + .contentTransition(.numericText()) + Text("Schritte heute") + .font(.subheadline) + .foregroundStyle(.secondary) + if manager.progress >= 1.0 { + Label("Ziel erreicht!", systemImage: "checkmark.circle.fill") + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(.green) + } + } + } + .frame(width: 260, height: 260) + .padding(.top, 8) + } + + private var statsGrid: some View { + LazyVGrid( + columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], + spacing: 12 + ) { + StatCard(icon: "flag.fill", color: .orange, value: "\(Int(manager.progress * 100)) %", label: "Fortschritt") + StatCard(icon: "figure.walk", color: .blue, value: String(format: "%.2f km", manager.distanceKm), label: "Distanz") + StatCard(icon: "flame.fill", color: .red, value: "\(manager.estimatedKcal) kcal", label: "Kalorien") + } + .padding(.horizontal) + } + + private var goalCard: some View { + VStack(alignment: .leading, spacing: 10) { + HStack { + Label("Tagesziel", systemImage: "target") + .font(.headline) + Spacer() + Button("Anpassen") { + goalDraft = Double(manager.stepGoal) + showGoalSheet = true + } + .buttonStyle(.bordered) + .tint(.blue) + } + ProgressView(value: manager.progress) + .tint(ringColor) + Text("\(manager.todaySteps.formatted()) / \(manager.stepGoal.formatted()) Schritte") + .font(.caption) + .foregroundStyle(.secondary) + } + .padding() + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16)) + .padding(.horizontal) + } + + private var deniedBanner: some View { + ContentUnavailableView( + "HealthKit-Zugriff verweigert", + systemImage: "heart.slash", + description: Text("Erlaube den Zugriff in Einstellungen > Datenschutz > Health.") + ) + .padding(.horizontal) + } +} + +// MARK: - Stat Card + +struct StatCard: View { + let icon: String + let color: Color + let value: String + let label: String + + var body: some View { + VStack(spacing: 6) { + Image(systemName: icon) + .font(.title3) + .foregroundStyle(color) + Text(value) + .font(.subheadline) + .fontWeight(.semibold) + .minimumScaleFactor(0.6) + .lineLimit(1) + Text(label) + .font(.caption2) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14)) + } +} + +// MARK: - Goal Editor Sheet + +struct GoalEditorSheet: View { + @Binding var goal: Double + @Environment(\.dismiss) private var dismiss + let onSave: () -> Void + + var body: some View { + NavigationStack { + Form { + Section("Tagesziel in Schritten") { + Slider(value: $goal, in: 1_000...30_000, step: 500) { + Text("Ziel") + } minimumValueLabel: { + Text("1K").font(.caption) + } maximumValueLabel: { + Text("30K").font(.caption) + } + Text("\(Int(goal).formatted()) Schritte") + .font(.title2.bold()) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.vertical, 4) + } + } + .navigationTitle("Ziel anpassen") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { Button("Abbrechen") { dismiss() } } + ToolbarItem(placement: .confirmationAction) { + Button("Speichern") { onSave(); dismiss() } + } + } + } + .presentationDetents([.medium]) + } +} + +#Preview { + ContentView() +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsApp/HealthKitManager.swift b/examples/ios-widget-steps/StepsWidget/StepsApp/HealthKitManager.swift new file mode 100644 index 0000000000..2a5518d0ee --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsApp/HealthKitManager.swift @@ -0,0 +1,80 @@ +import HealthKit +import WidgetKit + +// MARK: - HealthKit Manager + +@MainActor +final class HealthKitManager: ObservableObject { + private let store = HKHealthStore() + + @Published var todaySteps: Int = 0 + @Published var stepGoal: Int = 10_000 + @Published var authorizationStatus: AuthStatus = .notDetermined + + enum AuthStatus { case notDetermined, authorized, denied } + + private static let appGroup = "group.com.example.stepswidget" + private static let goalKey = "stepGoal" + + init() { + let stored = UserDefaults(suiteName: Self.appGroup)?.integer(forKey: Self.goalKey) ?? 0 + stepGoal = stored > 0 ? stored : 10_000 + } + + // MARK: - Authorization + + func requestAuthorization() { + guard HKHealthStore.isHealthDataAvailable() else { + authorizationStatus = .denied + return + } + let stepType = HKQuantityType(.stepCount) + store.requestAuthorization(toShare: [], read: [stepType]) { [weak self] success, _ in + Task { @MainActor [weak self] in + self?.authorizationStatus = success ? .authorized : .denied + if success { await self?.fetchTodaySteps() } + } + } + } + + // MARK: - Step Query + + func fetchTodaySteps() async { + guard HKHealthStore.isHealthDataAvailable() else { return } + let stepType = HKQuantityType(.stepCount) + let start = Calendar.current.startOfDay(for: .now) + let pred = HKQuery.predicateForSamples(withStart: start, end: .now, options: .strictStartDate) + + let steps: Int = await withCheckedContinuation { continuation in + let query = HKStatisticsQuery( + quantityType: stepType, + quantitySamplePredicate: pred, + options: .cumulativeSum + ) { _, result, _ in + let count = Int(result?.sumQuantity()?.doubleValue(for: .count()) ?? 0) + continuation.resume(returning: count) + } + store.execute(query) + } + todaySteps = steps + } + + // MARK: - Goal + + func updateGoal(_ newGoal: Int) { + stepGoal = newGoal + UserDefaults(suiteName: Self.appGroup)?.set(newGoal, forKey: Self.goalKey) + WidgetCenter.shared.reloadTimelines(ofKind: "StepsWidget") + } + + // MARK: - Derived values + + var progress: Double { + guard stepGoal > 0 else { return 0 } + return min(Double(todaySteps) / Double(stepGoal), 1.0) + } + + var distanceKm: Double { Double(todaySteps) * 0.000762 } + + var estimatedKcal: Int { Int(Double(todaySteps) * 0.04) } +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsApp/Info.plist b/examples/ios-widget-steps/StepsWidget/StepsApp/Info.plist new file mode 100644 index 0000000000..060331ecbe --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsApp/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHealthShareUsageDescription + StepsWidget liest deine Schrittzähler-Daten aus Apple Health, um sie in der App und im Widget anzuzeigen. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/ios-widget-steps/StepsWidget/StepsApp/StepsApp.entitlements b/examples/ios-widget-steps/StepsWidget/StepsApp/StepsApp.entitlements new file mode 100644 index 0000000000..96229d19d1 --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsApp/StepsApp.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.developer.healthkit + + com.apple.developer.healthkit.background-delivery + + com.apple.security.application-groups + + group.com.example.stepswidget + + + diff --git a/examples/ios-widget-steps/StepsWidget/StepsApp/StepsApp.swift b/examples/ios-widget-steps/StepsWidget/StepsApp/StepsApp.swift new file mode 100644 index 0000000000..c6f2b71764 --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsApp/StepsApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct StepsApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsWidget.xcodeproj/project.pbxproj b/examples/ios-widget-steps/StepsWidget/StepsWidget.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..c697c2b7a0 --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsWidget.xcodeproj/project.pbxproj @@ -0,0 +1,521 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + AA0000000000000000000211 /* StepsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000011 /* StepsApp.swift */; }; + AA0000000000000000000212 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000012 /* ContentView.swift */; }; + AA0000000000000000000213 /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000013 /* HealthKitManager.swift */; }; + AA0000000000000000000214 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000014 /* Assets.xcassets */; }; + AA0000000000000000000215 /* StepsWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000017 /* StepsWidgetBundle.swift */; }; + AA0000000000000000000216 /* StepsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000018 /* StepsProvider.swift */; }; + AA0000000000000000000217 /* StepsWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000019 /* StepsWidgetView.swift */; }; + AA0000000000000000000218 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA000000000000000000001A /* Assets.xcassets */; }; + AA0000000000000000000219 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA000000000000000000001D /* HealthKit.framework */; }; + AA000000000000000000021A /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA000000000000000000001D /* HealthKit.framework */; }; + AA000000000000000000021B /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA000000000000000000001E /* WidgetKit.framework */; }; + AA000000000000000000021C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA000000000000000000001F /* SwiftUI.framework */; }; + AA000000000000000000021D /* StepsWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000021 /* StepsWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + AA000000000000000000021E /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA000000000000000000001E /* WidgetKit.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + AA0000000000000000000072 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AA0000000000000000000001 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AA0000000000000000000003; + remoteInfo = StepsWidgetExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + AA0000000000000000000044 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + AA000000000000000000021D /* StepsWidgetExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + AA0000000000000000000011 /* StepsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepsApp.swift; sourceTree = ""; }; + AA0000000000000000000012 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + AA0000000000000000000013 /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = ""; }; + AA0000000000000000000014 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AA0000000000000000000015 /* StepsApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = StepsApp.entitlements; sourceTree = ""; }; + AA0000000000000000000016 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AA0000000000000000000017 /* StepsWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepsWidgetBundle.swift; sourceTree = ""; }; + AA0000000000000000000018 /* StepsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepsProvider.swift; sourceTree = ""; }; + AA0000000000000000000019 /* StepsWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepsWidgetView.swift; sourceTree = ""; }; + AA000000000000000000001A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AA000000000000000000001B /* StepsWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = StepsWidgetExtension.entitlements; sourceTree = ""; }; + AA000000000000000000001C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AA000000000000000000001D /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; }; + AA000000000000000000001E /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + AA000000000000000000001F /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + AA0000000000000000000020 /* StepsApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StepsApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AA0000000000000000000021 /* StepsWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = StepsWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AA0000000000000000000042 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AA0000000000000000000219 /* HealthKit.framework in Frameworks */, + AA000000000000000000021E /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA0000000000000000000046 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AA000000000000000000021A /* HealthKit.framework in Frameworks */, + AA000000000000000000021B /* WidgetKit.framework in Frameworks */, + AA000000000000000000021C /* SwiftUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + AA0000000000000000000031 = { + isa = PBXGroup; + children = ( + AA0000000000000000000032 /* StepsApp */, + AA0000000000000000000033 /* StepsWidgetExtension */, + AA0000000000000000000034 /* Frameworks */, + AA0000000000000000000035 /* Products */, + ); + sourceTree = ""; + }; + AA0000000000000000000032 /* StepsApp */ = { + isa = PBXGroup; + children = ( + AA0000000000000000000011 /* StepsApp.swift */, + AA0000000000000000000012 /* ContentView.swift */, + AA0000000000000000000013 /* HealthKitManager.swift */, + AA0000000000000000000014 /* Assets.xcassets */, + AA0000000000000000000015 /* StepsApp.entitlements */, + AA0000000000000000000016 /* Info.plist */, + ); + path = StepsApp; + sourceTree = ""; + }; + AA0000000000000000000033 /* StepsWidgetExtension */ = { + isa = PBXGroup; + children = ( + AA0000000000000000000017 /* StepsWidgetBundle.swift */, + AA0000000000000000000018 /* StepsProvider.swift */, + AA0000000000000000000019 /* StepsWidgetView.swift */, + AA000000000000000000001A /* Assets.xcassets */, + AA000000000000000000001B /* StepsWidgetExtension.entitlements */, + AA000000000000000000001C /* Info.plist */, + ); + path = StepsWidgetExtension; + sourceTree = ""; + }; + AA0000000000000000000034 /* Frameworks */ = { + isa = PBXGroup; + children = ( + AA000000000000000000001D /* HealthKit.framework */, + AA000000000000000000001E /* WidgetKit.framework */, + AA000000000000000000001F /* SwiftUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + AA0000000000000000000035 /* Products */ = { + isa = PBXGroup; + children = ( + AA0000000000000000000020 /* StepsApp.app */, + AA0000000000000000000021 /* StepsWidgetExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AA0000000000000000000002 /* StepsApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = AA0000000000000000000062 /* Build configuration list for PBXNativeTarget "StepsApp" */; + buildPhases = ( + AA0000000000000000000041 /* Sources */, + AA0000000000000000000042 /* Frameworks */, + AA0000000000000000000043 /* Resources */, + AA0000000000000000000044 /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + AA0000000000000000000071 /* PBXTargetDependency */, + ); + name = StepsApp; + productName = StepsApp; + productReference = AA0000000000000000000020 /* StepsApp.app */; + productType = "com.apple.product-type.application"; + }; + AA0000000000000000000003 /* StepsWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = AA0000000000000000000063 /* Build configuration list for PBXNativeTarget "StepsWidgetExtension" */; + buildPhases = ( + AA0000000000000000000045 /* Sources */, + AA0000000000000000000046 /* Frameworks */, + AA0000000000000000000047 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = StepsWidgetExtension; + productName = StepsWidgetExtension; + productReference = AA0000000000000000000021 /* StepsWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AA0000000000000000000001 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + AA0000000000000000000002 = { + CreatedOnToolsVersion = 16.0; + }; + AA0000000000000000000003 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = AA0000000000000000000061 /* Build configuration list for PBXProject "StepsWidget" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AA0000000000000000000031; + productRefGroup = AA0000000000000000000035 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AA0000000000000000000002 /* StepsApp */, + AA0000000000000000000003 /* StepsWidgetExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AA0000000000000000000043 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA0000000000000000000214 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA0000000000000000000047 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA0000000000000000000218 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AA0000000000000000000041 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA0000000000000000000211 /* StepsApp.swift in Sources */, + AA0000000000000000000212 /* ContentView.swift in Sources */, + AA0000000000000000000213 /* HealthKitManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA0000000000000000000045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AA0000000000000000000215 /* StepsWidgetBundle.swift in Sources */, + AA0000000000000000000216 /* StepsProvider.swift in Sources */, + AA0000000000000000000217 /* StepsWidgetView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + AA0000000000000000000071 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AA0000000000000000000003 /* StepsWidgetExtension */; + targetProxy = AA0000000000000000000072 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + AA0000000000000000000051 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + AA0000000000000000000052 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AA0000000000000000000053 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = StepsApp/StepsApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = StepsApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.stepswidget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AA0000000000000000000054 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = StepsApp/StepsApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = StepsApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.stepswidget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + AA0000000000000000000055 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = StepsWidgetExtension/StepsWidgetExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = StepsWidgetExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.stepswidget.widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AA0000000000000000000056 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = StepsWidgetExtension/StepsWidgetExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = StepsWidgetExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.stepswidget.widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AA0000000000000000000061 /* Build configuration list for PBXProject "StepsWidget" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA0000000000000000000051 /* Debug */, + AA0000000000000000000052 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AA0000000000000000000062 /* Build configuration list for PBXNativeTarget "StepsApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA0000000000000000000053 /* Debug */, + AA0000000000000000000054 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AA0000000000000000000063 /* Build configuration list for PBXNativeTarget "StepsWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AA0000000000000000000055 /* Debug */, + AA0000000000000000000056 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + + }; + rootObject = AA0000000000000000000001 /* Project object */; +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..805e83152a --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.478", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.478", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Assets.xcassets/Contents.json b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000000..fe3de53757 --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.118", + "green" : "0.118", + "red" : "0.118" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Info.plist b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Info.plist new file mode 100644 index 0000000000..1cad92f721 --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/Info.plist @@ -0,0 +1,15 @@ + + + + + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsProvider.swift b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsProvider.swift new file mode 100644 index 0000000000..6429f29645 --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsProvider.swift @@ -0,0 +1,86 @@ +import WidgetKit +@preconcurrency import HealthKit + +// MARK: - Timeline Entry + +struct StepsEntry: TimelineEntry { + let date: Date + let steps: Int + let goal: Int + + var progress: Double { + guard goal > 0 else { return 0 } + return min(Double(steps) / Double(goal), 1.0) + } + + var distanceKm: Double { Double(steps) * 0.000762 } + var isGoalReached: Bool { steps >= goal } + + static let placeholder = StepsEntry(date: .now, steps: 7_432, goal: 10_000) +} + +// MARK: - Timeline Provider + +struct StepsProvider: TimelineProvider { + private static let appGroup = "group.com.example.stepswidget" + private static let goalKey = "stepGoal" + + func placeholder(in context: Context) -> StepsEntry { .placeholder } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + guard !context.isPreview else { completion(.placeholder); return } + fetchEntry(completion: completion) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + fetchEntry { entry in + let next = Calendar.current.date(byAdding: .minute, value: 15, to: .now)! + completion(Timeline(entries: [entry], policy: .after(next))) + } + } + + // MARK: - Private + + private func fetchEntry(completion: @escaping (StepsEntry) -> Void) { + let goal = loadGoal() + + guard HKHealthStore.isHealthDataAvailable() else { + completion(StepsEntry(date: .now, steps: 0, goal: goal)) + return + } + + let store = HKHealthStore() + let stepType = HKQuantityType(.stepCount) + + store.requestAuthorization(toShare: [], read: [stepType]) { success, _ in + guard success else { + completion(StepsEntry(date: .now, steps: 0, goal: goal)) + return + } + querySteps(store: store) { steps in + completion(StepsEntry(date: .now, steps: steps, goal: goal)) + } + } + } + + private func querySteps(store: HKHealthStore, completion: @escaping (Int) -> Void) { + let stepType = HKQuantityType(.stepCount) + let start = Calendar.current.startOfDay(for: Date()) + let pred = HKQuery.predicateForSamples(withStart: start, end: Date(), options: .strictStartDate) + + let query = HKStatisticsQuery( + quantityType: stepType, + quantitySamplePredicate: pred, + options: .cumulativeSum + ) { _, result, _ in + let steps = Int(result?.sumQuantity()?.doubleValue(for: .count()) ?? 0) + completion(steps) + } + store.execute(query) + } + + private func loadGoal() -> Int { + let stored = UserDefaults(suiteName: Self.appGroup)?.integer(forKey: Self.goalKey) ?? 0 + return stored > 0 ? stored : 10_000 + } +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsWidgetBundle.swift b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsWidgetBundle.swift new file mode 100644 index 0000000000..7c2ca3038c --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsWidgetBundle.swift @@ -0,0 +1,9 @@ +import WidgetKit +import SwiftUI + +@main +struct StepsWidgetBundle: WidgetBundle { + var body: some Widget { + StepsWidget() + } +} diff --git a/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsWidgetExtension.entitlements b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsWidgetExtension.entitlements new file mode 100644 index 0000000000..107b9a68bc --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsWidgetExtension.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.developer.healthkit + + com.apple.security.application-groups + + group.com.example.stepswidget + + + diff --git a/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsWidgetView.swift b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsWidgetView.swift new file mode 100644 index 0000000000..18c572fd1f --- /dev/null +++ b/examples/ios-widget-steps/StepsWidget/StepsWidgetExtension/StepsWidgetView.swift @@ -0,0 +1,234 @@ +import SwiftUI +import WidgetKit + +// MARK: - Widget Configuration + +struct StepsWidget: Widget { + let kind = "StepsWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: StepsProvider()) { entry in + StepsWidgetRootView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } + .configurationDisplayName("Schritte") + .description("Zeigt deine heutigen Schritte aus Apple Health.") + .supportedFamilies([ + .systemSmall, + .systemMedium, + .accessoryCircular, + .accessoryRectangular, + .accessoryInline, + ]) + } +} + +// MARK: - Root View + +struct StepsWidgetRootView: View { + @Environment(\.widgetFamily) var family + let entry: StepsEntry + + var body: some View { + switch family { + case .systemSmall: SmallStepsView(entry: entry) + case .systemMedium: MediumStepsView(entry: entry) + case .accessoryCircular: AccessoryCircularView(entry: entry) + case .accessoryRectangular: AccessoryRectangularView(entry: entry) + case .accessoryInline: AccessoryInlineView(entry: entry) + default: SmallStepsView(entry: entry) + } + } +} + +// MARK: - Small Widget (progress ring + step count) + +struct SmallStepsView: View { + let entry: StepsEntry + var ringColor: Color { entry.isGoalReached ? .green : .blue } + + var body: some View { + ZStack { + Circle() + .stroke(ringColor.opacity(0.18), lineWidth: 12) + .padding(14) + Circle() + .trim(from: 0, to: entry.progress) + .stroke(ringColor, style: StrokeStyle(lineWidth: 12, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .padding(14) + + VStack(spacing: 2) { + Image(systemName: "figure.walk") + .font(.caption2) + .foregroundStyle(ringColor) + Text(stepsLabel) + .font(.system(size: 22, weight: .bold, design: .rounded)) + .minimumScaleFactor(0.5) + .lineLimit(1) + Text("Schritte") + .font(.system(size: 9)) + .foregroundStyle(.secondary) + if entry.isGoalReached { + Image(systemName: "checkmark.circle.fill") + .font(.caption2) + .foregroundStyle(.green) + .padding(.top, 1) + } + } + } + } + + private var stepsLabel: String { + entry.steps >= 10_000 + ? String(format: "%.1fK", Double(entry.steps) / 1_000) + : entry.steps.formatted() + } +} + +// MARK: - Medium Widget (ring + stats) + +struct MediumStepsView: View { + let entry: StepsEntry + var ringColor: Color { entry.isGoalReached ? .green : .blue } + + var body: some View { + HStack(spacing: 16) { + // Progress ring + ZStack { + Circle() + .stroke(ringColor.opacity(0.18), lineWidth: 11) + Circle() + .trim(from: 0, to: entry.progress) + .stroke(ringColor, style: StrokeStyle(lineWidth: 11, lineCap: .round)) + .rotationEffect(.degrees(-90)) + VStack(spacing: 1) { + Text(entry.steps.formatted()) + .font(.system(size: 18, weight: .bold, design: .rounded)) + .minimumScaleFactor(0.5) + .lineLimit(1) + Text("\(Int(entry.progress * 100)) %") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .frame(width: 90, height: 90) + + // Stats + VStack(alignment: .leading, spacing: 7) { + StepStatRow(icon: "target", color: .orange, text: "Ziel: \(entry.goal.formatted())") + StepStatRow(icon: "figure.walk", color: .blue, text: String(format: "%.2f km", entry.distanceKm)) + ProgressView(value: entry.progress).tint(ringColor) + + HStack { + if entry.isGoalReached { + Label("Ziel erreicht!", systemImage: "star.fill") + .font(.caption2).fontWeight(.semibold).foregroundStyle(.green) + } + Spacer() + Text(entry.date, style: .relative) + .font(.caption2).foregroundStyle(.tertiary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(14) + } +} + +private struct StepStatRow: View { + let icon: String + let color: Color + let text: String + + var body: some View { + Label { + Text(text).font(.caption) + } icon: { + Image(systemName: icon).foregroundStyle(color) + } + } +} + +// MARK: - Accessory Circular (Lock Screen – Gauge) + +struct AccessoryCircularView: View { + let entry: StepsEntry + + var body: some View { + Gauge(value: entry.progress) { + Image(systemName: "figure.walk") + } currentValueLabel: { + Text(compactSteps) + .font(.system(size: 11, weight: .bold, design: .rounded)) + } + .gaugeStyle(.accessoryCircular) + .tint(entry.isGoalReached ? .green : .blue) + } + + private var compactSteps: String { + entry.steps >= 1_000 + ? String(format: "%.1fK", Double(entry.steps) / 1_000) + : "\(entry.steps)" + } +} + +// MARK: - Accessory Rectangular (Lock Screen) + +struct AccessoryRectangularView: View { + let entry: StepsEntry + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + Label("Schritte heute", systemImage: "figure.walk") + .font(.caption2) + .foregroundStyle(.secondary) + Text(entry.steps.formatted()) + .font(.system(.headline, design: .rounded)) + .fontWeight(.bold) + ProgressView(value: entry.progress) + .tint(entry.isGoalReached ? .green : .blue) + } + } +} + +// MARK: - Accessory Inline (Lock Screen) + +struct AccessoryInlineView: View { + let entry: StepsEntry + + var body: some View { + Label( + "\(entry.steps.formatted()) / \(entry.goal.formatted())", + systemImage: "figure.walk" + ) + } +} + +// MARK: - Previews + +#Preview("Small", as: .systemSmall) { + StepsWidget() +} timeline: { + StepsEntry.placeholder + StepsEntry(date: .now, steps: 10_823, goal: 10_000) +} + +#Preview("Medium", as: .systemMedium) { + StepsWidget() +} timeline: { + StepsEntry.placeholder + StepsEntry(date: .now, steps: 10_823, goal: 10_000) +} + +#Preview("Circular", as: .accessoryCircular) { + StepsWidget() +} timeline: { + StepsEntry.placeholder +} + +#Preview("Rectangular", as: .accessoryRectangular) { + StepsWidget() +} timeline: { + StepsEntry.placeholder +} diff --git a/examples/ios-widget/ClaudeWidget/ClaudeWidget.xcodeproj/project.pbxproj b/examples/ios-widget/ClaudeWidget/ClaudeWidget.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..7f25c5b16f --- /dev/null +++ b/examples/ios-widget/ClaudeWidget/ClaudeWidget.xcodeproj/project.pbxproj @@ -0,0 +1,387 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + A11A000001 /* ClaudeWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10A000001 /* ClaudeWidgetBundle.swift */; }; + A11A000002 /* WidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10A000002 /* WidgetData.swift */; }; + A11A000003 /* ClaudeStatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10A000003 /* ClaudeStatusProvider.swift */; }; + A11A000004 /* ClaudeSessionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10A000004 /* ClaudeSessionProvider.swift */; }; + A11A000005 /* ClaudeStatusWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10A000005 /* ClaudeStatusWidgetView.swift */; }; + A11A000006 /* ClaudeSessionWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10A000006 /* ClaudeSessionWidgetView.swift */; }; + A11A000007 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A10A000007 /* Assets.xcassets */; }; + A11A000008 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A10A000008 /* WidgetKit.framework */; }; + A11A000009 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A10A000009 /* SwiftUI.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A10A000001 /* ClaudeWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeWidgetBundle.swift; sourceTree = ""; }; + A10A000002 /* WidgetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetData.swift; sourceTree = ""; }; + A10A000003 /* ClaudeStatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeStatusProvider.swift; sourceTree = ""; }; + A10A000004 /* ClaudeSessionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeSessionProvider.swift; sourceTree = ""; }; + A10A000005 /* ClaudeStatusWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeStatusWidgetView.swift; sourceTree = ""; }; + A10A000006 /* ClaudeSessionWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeSessionWidgetView.swift; sourceTree = ""; }; + A10A000007 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A10A000008 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + A10A000009 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + A10A00000A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A10A00000B /* ClaudeWidget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ClaudeWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A1FB000001 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A11A000008 /* WidgetKit.framework in Frameworks */, + A11A000009 /* SwiftUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A1GRP00001 /* ClaudeWidget */ = { + isa = PBXGroup; + children = ( + A1GRP00002 /* WidgetExtension */, + A1GRP00005 /* Frameworks */, + A1GRP00006 /* Products */, + ); + sourceTree = ""; + }; + A1GRP00002 /* WidgetExtension */ = { + isa = PBXGroup; + children = ( + A10A000001 /* ClaudeWidgetBundle.swift */, + A10A00000A /* Info.plist */, + A10A000007 /* Assets.xcassets */, + A1GRP00003 /* Model */, + A1GRP00004 /* Provider */, + A1GRP00007 /* Views */, + ); + path = WidgetExtension; + sourceTree = ""; + }; + A1GRP00003 /* Model */ = { + isa = PBXGroup; + children = ( + A10A000002 /* WidgetData.swift */, + ); + path = Model; + sourceTree = ""; + }; + A1GRP00004 /* Provider */ = { + isa = PBXGroup; + children = ( + A10A000003 /* ClaudeStatusProvider.swift */, + A10A000004 /* ClaudeSessionProvider.swift */, + ); + path = Provider; + sourceTree = ""; + }; + A1GRP00007 /* Views */ = { + isa = PBXGroup; + children = ( + A10A000005 /* ClaudeStatusWidgetView.swift */, + A10A000006 /* ClaudeSessionWidgetView.swift */, + ); + path = Views; + sourceTree = ""; + }; + A1GRP00005 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A10A000008 /* WidgetKit.framework */, + A10A000009 /* SwiftUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + A1GRP00006 /* Products */ = { + isa = PBXGroup; + children = ( + A10A00000B /* ClaudeWidget.appex */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A1TGT00001 /* ClaudeWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = A1BCL00001 /* Build configuration list for PBXNativeTarget "ClaudeWidgetExtension" */; + buildPhases = ( + A1SRC00001 /* Sources */, + A1FB000001 /* Frameworks */, + A1RES00001 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ClaudeWidgetExtension; + productName = ClaudeWidget; + productReference = A10A00000B /* ClaudeWidget.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A1PRJ00001 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + A1TGT00001 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = A1BCL00002 /* Build configuration list for PBXProject "ClaudeWidget" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A1GRP00001 /* ClaudeWidget */; + productRefGroup = A1GRP00006 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A1TGT00001 /* ClaudeWidgetExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A1RES00001 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A11A000007 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A1SRC00001 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A11A000001 /* ClaudeWidgetBundle.swift in Sources */, + A11A000002 /* WidgetData.swift in Sources */, + A11A000003 /* ClaudeStatusProvider.swift in Sources */, + A11A000004 /* ClaudeSessionProvider.swift in Sources */, + A11A000005 /* ClaudeStatusWidgetView.swift in Sources */, + A11A000006 /* ClaudeSessionWidgetView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A1BC000001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A1BC000002 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A1BC000003 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = WidgetExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.anthropic.claude-code.widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A1BC000004 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = WidgetExtension/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.anthropic.claude-code.widget"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A1BCL00001 /* Build configuration list for PBXNativeTarget "ClaudeWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A1BC000003 /* Debug */, + A1BC000004 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A1BCL00002 /* Build configuration list for PBXProject "ClaudeWidget" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A1BC000001 /* Debug */, + A1BC000002 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + + }; + rootObject = A1PRJ00001 /* Project object */; +} diff --git a/examples/ios-widget/ClaudeWidget/WidgetExtension/Assets.xcassets/Contents.json b/examples/ios-widget/ClaudeWidget/WidgetExtension/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/examples/ios-widget/ClaudeWidget/WidgetExtension/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/ios-widget/ClaudeWidget/WidgetExtension/ClaudeWidgetBundle.swift b/examples/ios-widget/ClaudeWidget/WidgetExtension/ClaudeWidgetBundle.swift new file mode 100644 index 0000000000..6737759908 --- /dev/null +++ b/examples/ios-widget/ClaudeWidget/WidgetExtension/ClaudeWidgetBundle.swift @@ -0,0 +1,10 @@ +import WidgetKit +import SwiftUI + +@main +struct ClaudeWidgetBundle: WidgetBundle { + var body: some Widget { + ClaudeStatusWidget() + ClaudeSessionWidget() + } +} diff --git a/examples/ios-widget/ClaudeWidget/WidgetExtension/Info.plist b/examples/ios-widget/ClaudeWidget/WidgetExtension/Info.plist new file mode 100644 index 0000000000..250057eded --- /dev/null +++ b/examples/ios-widget/ClaudeWidget/WidgetExtension/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/examples/ios-widget/ClaudeWidget/WidgetExtension/Model/WidgetData.swift b/examples/ios-widget/ClaudeWidget/WidgetExtension/Model/WidgetData.swift new file mode 100644 index 0000000000..004ff50a3b --- /dev/null +++ b/examples/ios-widget/ClaudeWidget/WidgetExtension/Model/WidgetData.swift @@ -0,0 +1,71 @@ +import Foundation + +// MARK: - Widget Data Models + +struct ClaudeSessionData: Codable { + let totalSessions: Int + let linesChanged: Int + let filesEdited: Int + let tasksCompleted: Int + let lastActive: Date + let activeProject: String + let modelUsed: String + + static let placeholder = ClaudeSessionData( + totalSessions: 42, + linesChanged: 1_337, + filesEdited: 28, + tasksCompleted: 15, + lastActive: .now, + activeProject: "claude-code", + modelUsed: "claude-sonnet-4-6" + ) + + static let empty = ClaudeSessionData( + totalSessions: 0, + linesChanged: 0, + filesEdited: 0, + tasksCompleted: 0, + lastActive: .now, + activeProject: "–", + modelUsed: "–" + ) +} + +struct ClaudeStatusData: Codable { + let isConnected: Bool + let apiStatus: APIStatus + let currentModel: String + let remainingTokens: Int? + let lastUpdated: Date + + enum APIStatus: String, Codable { + case operational = "Operational" + case degraded = "Degraded" + case outage = "Outage" + + var color: String { + switch self { + case .operational: return "green" + case .degraded: return "yellow" + case .outage: return "red" + } + } + } + + static let placeholder = ClaudeStatusData( + isConnected: true, + apiStatus: .operational, + currentModel: "claude-sonnet-4-6", + remainingTokens: 45_000, + lastUpdated: .now + ) +} + +// MARK: - Shared App Group + +extension UserDefaults { + static let widgetGroup = UserDefaults( + suiteName: "group.com.anthropic.claude-code.widget" + ) +} diff --git a/examples/ios-widget/ClaudeWidget/WidgetExtension/Provider/ClaudeSessionProvider.swift b/examples/ios-widget/ClaudeWidget/WidgetExtension/Provider/ClaudeSessionProvider.swift new file mode 100644 index 0000000000..3988b955bd --- /dev/null +++ b/examples/ios-widget/ClaudeWidget/WidgetExtension/Provider/ClaudeSessionProvider.swift @@ -0,0 +1,60 @@ +import WidgetKit +import Foundation + +// MARK: - Timeline Entry + +struct ClaudeSessionEntry: TimelineEntry { + let date: Date + let session: ClaudeSessionData + let isPlaceholder: Bool + + static let placeholder = ClaudeSessionEntry( + date: .now, + session: .placeholder, + isPlaceholder: true + ) +} + +// MARK: - Timeline Provider + +struct ClaudeSessionProvider: TimelineProvider { + typealias Entry = ClaudeSessionEntry + + func placeholder(in context: Context) -> ClaudeSessionEntry { + .placeholder + } + + func getSnapshot(in context: Context, completion: @escaping (ClaudeSessionEntry) -> Void) { + let entry = ClaudeSessionEntry( + date: .now, + session: loadSession(), + isPlaceholder: context.isPreview + ) + completion(entry) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + let currentDate = Date.now + let session = loadSession() + + let entry = ClaudeSessionEntry(date: currentDate, session: session, isPlaceholder: false) + + // Refresh every 30 minutes + let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)! + let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) + completion(timeline) + } + + // MARK: - Private + + private func loadSession() -> ClaudeSessionData { + guard + let defaults = UserDefaults.widgetGroup, + let data = defaults.data(forKey: "claudeSession"), + let decoded = try? JSONDecoder().decode(ClaudeSessionData.self, from: data) + else { + return .placeholder + } + return decoded + } +} diff --git a/examples/ios-widget/ClaudeWidget/WidgetExtension/Provider/ClaudeStatusProvider.swift b/examples/ios-widget/ClaudeWidget/WidgetExtension/Provider/ClaudeStatusProvider.swift new file mode 100644 index 0000000000..c769e2ba2c --- /dev/null +++ b/examples/ios-widget/ClaudeWidget/WidgetExtension/Provider/ClaudeStatusProvider.swift @@ -0,0 +1,60 @@ +import WidgetKit +import Foundation + +// MARK: - Timeline Entry + +struct ClaudeStatusEntry: TimelineEntry { + let date: Date + let status: ClaudeStatusData + let isPlaceholder: Bool + + static let placeholder = ClaudeStatusEntry( + date: .now, + status: .placeholder, + isPlaceholder: true + ) +} + +// MARK: - Timeline Provider + +struct ClaudeStatusProvider: TimelineProvider { + typealias Entry = ClaudeStatusEntry + + func placeholder(in context: Context) -> ClaudeStatusEntry { + .placeholder + } + + func getSnapshot(in context: Context, completion: @escaping (ClaudeStatusEntry) -> Void) { + let entry = ClaudeStatusEntry( + date: .now, + status: loadStatus(), + isPlaceholder: context.isPreview + ) + completion(entry) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + let currentDate = Date.now + let status = loadStatus() + + let entry = ClaudeStatusEntry(date: currentDate, status: status, isPlaceholder: false) + + // Refresh every 15 minutes + let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)! + let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) + completion(timeline) + } + + // MARK: - Private + + private func loadStatus() -> ClaudeStatusData { + guard + let defaults = UserDefaults.widgetGroup, + let data = defaults.data(forKey: "claudeStatus"), + let decoded = try? JSONDecoder().decode(ClaudeStatusData.self, from: data) + else { + return .placeholder + } + return decoded + } +} diff --git a/examples/ios-widget/ClaudeWidget/WidgetExtension/Views/ClaudeSessionWidgetView.swift b/examples/ios-widget/ClaudeWidget/WidgetExtension/Views/ClaudeSessionWidgetView.swift new file mode 100644 index 0000000000..db5ea73955 --- /dev/null +++ b/examples/ios-widget/ClaudeWidget/WidgetExtension/Views/ClaudeSessionWidgetView.swift @@ -0,0 +1,272 @@ +import SwiftUI +import WidgetKit + +// MARK: - Session Widget + +struct ClaudeSessionWidget: Widget { + let kind = "ClaudeSessionWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: ClaudeSessionProvider()) { entry in + ClaudeSessionWidgetView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } + .configurationDisplayName("Claude Session Stats") + .description("Shows your Claude Code session statistics at a glance.") + .supportedFamilies([ + .systemSmall, + .systemMedium, + .systemLarge + ]) + } +} + +// MARK: - View + +struct ClaudeSessionWidgetView: View { + @Environment(\.widgetFamily) var family + var entry: ClaudeSessionEntry + + var body: some View { + switch family { + case .systemSmall: + SmallSessionView(entry: entry) + case .systemMedium: + MediumSessionView(entry: entry) + case .systemLarge: + LargeSessionView(entry: entry) + default: + SmallSessionView(entry: entry) + } + } +} + +// MARK: - Shared stat tile + +struct StatTile: View { + let icon: String + let iconColor: Color + let value: String + let label: String + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Image(systemName: icon) + .foregroundStyle(iconColor) + .font(.caption) + Text(value) + .font(.headline) + .fontWeight(.semibold) + .minimumScaleFactor(0.7) + .lineLimit(1) + Text(label) + .font(.caption2) + .foregroundStyle(.secondary) + } + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12)) + } +} + +// MARK: - Small + +struct SmallSessionView: View { + let entry: ClaudeSessionEntry + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + HStack { + Image(systemName: "terminal.fill") + .foregroundStyle(.purple) + Text("Sessions") + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + } + + Text("\(entry.session.totalSessions)") + .font(.system(size: 36, weight: .bold, design: .rounded)) + .foregroundStyle(.purple) + + Divider() + + HStack { + Image(systemName: "doc.text.fill") + .foregroundStyle(.blue) + .font(.caption) + Text("\(entry.session.filesEdited) files") + .font(.caption) + Spacer() + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.green) + .font(.caption) + Text("\(entry.session.tasksCompleted) tasks") + .font(.caption) + } + + Text(entry.session.activeProject) + .font(.caption2) + .foregroundStyle(.secondary) + .lineLimit(1) + } + .padding(12) + } +} + +// MARK: - Medium + +struct MediumSessionView: View { + let entry: ClaudeSessionEntry + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "terminal.fill") + .foregroundStyle(.purple) + Text("Claude Code Stats") + .font(.headline) + .fontWeight(.semibold) + Spacer() + Text(entry.session.activeProject) + .font(.caption) + .foregroundStyle(.secondary) + } + + HStack(spacing: 8) { + StatTile( + icon: "play.circle.fill", + iconColor: .purple, + value: "\(entry.session.totalSessions)", + label: "Sessions" + ) + StatTile( + icon: "doc.fill", + iconColor: .blue, + value: "\(entry.session.filesEdited)", + label: "Files" + ) + StatTile( + icon: "checkmark.seal.fill", + iconColor: .green, + value: "\(entry.session.tasksCompleted)", + label: "Tasks" + ) + StatTile( + icon: "pencil", + iconColor: .orange, + value: entry.session.linesChanged.formatted(.number.notation(.compactName)), + label: "Lines" + ) + } + } + .padding(14) + } +} + +// MARK: - Large + +struct LargeSessionView: View { + let entry: ClaudeSessionEntry + + private var relativeDate: String { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .short + return formatter.localizedString(for: entry.session.lastActive, relativeTo: .now) + } + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + // Header + HStack { + HStack(spacing: 8) { + Image(systemName: "terminal.fill") + .foregroundStyle(.purple) + .font(.title3) + VStack(alignment: .leading, spacing: 1) { + Text("Claude Code") + .font(.headline) + .fontWeight(.bold) + Text(entry.session.activeProject) + .font(.caption) + .foregroundStyle(.secondary) + } + } + Spacer() + Text("Active \(relativeDate)") + .font(.caption) + .foregroundStyle(.secondary) + } + + Divider() + + // Stat grid (2x2) + LazyVGrid( + columns: [GridItem(.flexible()), GridItem(.flexible())], + spacing: 8 + ) { + StatTile( + icon: "play.circle.fill", + iconColor: .purple, + value: "\(entry.session.totalSessions)", + label: "Total Sessions" + ) + StatTile( + icon: "doc.fill", + iconColor: .blue, + value: "\(entry.session.filesEdited)", + label: "Files Edited" + ) + StatTile( + icon: "checkmark.seal.fill", + iconColor: .green, + value: "\(entry.session.tasksCompleted)", + label: "Tasks Completed" + ) + StatTile( + icon: "pencil.and.outline", + iconColor: .orange, + value: entry.session.linesChanged.formatted(.number.notation(.compactName)), + label: "Lines Changed" + ) + } + + Divider() + + // Model info + HStack { + Image(systemName: "cpu.fill") + .foregroundStyle(.purple) + .font(.caption) + Text(entry.session.modelUsed) + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Text("Updated \(entry.date, style: .relative) ago") + .font(.caption2) + .foregroundStyle(.tertiary) + } + } + .padding(16) + } +} + +// MARK: - Previews + +#Preview(as: .systemSmall) { + ClaudeSessionWidget() +} timeline: { + ClaudeSessionEntry.placeholder +} + +#Preview(as: .systemMedium) { + ClaudeSessionWidget() +} timeline: { + ClaudeSessionEntry.placeholder +} + +#Preview(as: .systemLarge) { + ClaudeSessionWidget() +} timeline: { + ClaudeSessionEntry.placeholder +} diff --git a/examples/ios-widget/ClaudeWidget/WidgetExtension/Views/ClaudeStatusWidgetView.swift b/examples/ios-widget/ClaudeWidget/WidgetExtension/Views/ClaudeStatusWidgetView.swift new file mode 100644 index 0000000000..782b758281 --- /dev/null +++ b/examples/ios-widget/ClaudeWidget/WidgetExtension/Views/ClaudeStatusWidgetView.swift @@ -0,0 +1,220 @@ +import SwiftUI +import WidgetKit + +// MARK: - Status Widget + +struct ClaudeStatusWidget: Widget { + let kind = "ClaudeStatusWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: ClaudeStatusProvider()) { entry in + ClaudeStatusWidgetView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } + .configurationDisplayName("Claude Status") + .description("Shows the current Claude API status and active model.") + .supportedFamilies([ + .systemSmall, + .systemMedium, + .accessoryCircular, + .accessoryRectangular, + .accessoryInline + ]) + } +} + +// MARK: - View + +struct ClaudeStatusWidgetView: View { + @Environment(\.widgetFamily) var family + var entry: ClaudeStatusEntry + + var body: some View { + switch family { + case .systemSmall: + SmallStatusView(entry: entry) + case .systemMedium: + MediumStatusView(entry: entry) + case .accessoryCircular: + AccessoryCircularStatusView(entry: entry) + case .accessoryRectangular: + AccessoryRectangularStatusView(entry: entry) + case .accessoryInline: + AccessoryInlineStatusView(entry: entry) + default: + SmallStatusView(entry: entry) + } + } +} + +// MARK: - Small Widget + +struct SmallStatusView: View { + let entry: ClaudeStatusEntry + + var statusColor: Color { + switch entry.status.apiStatus { + case .operational: return .green + case .degraded: return .yellow + case .outage: return .red + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "cpu.fill") + .foregroundStyle(.purple) + Spacer() + Circle() + .fill(statusColor) + .frame(width: 10, height: 10) + } + + Spacer() + + Text("Claude") + .font(.headline) + .fontWeight(.bold) + + Text(entry.status.apiStatus.rawValue) + .font(.caption) + .foregroundStyle(statusColor) + + Text(entry.status.currentModel) + .font(.caption2) + .foregroundStyle(.secondary) + .lineLimit(1) + .minimumScaleFactor(0.7) + } + .padding(12) + } +} + +// MARK: - Medium Widget + +struct MediumStatusView: View { + let entry: ClaudeStatusEntry + + var statusColor: Color { + switch entry.status.apiStatus { + case .operational: return .green + case .degraded: return .yellow + case .outage: return .red + } + } + + var body: some View { + HStack(spacing: 16) { + // Left: Icon + status + VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 6) { + Image(systemName: "cpu.fill") + .foregroundStyle(.purple) + .font(.title3) + Text("Claude Code") + .font(.headline) + .fontWeight(.bold) + } + + HStack(spacing: 4) { + Circle() + .fill(statusColor) + .frame(width: 8, height: 8) + Text(entry.status.apiStatus.rawValue) + .font(.subheadline) + .foregroundStyle(statusColor) + } + + Text(entry.status.currentModel) + .font(.caption) + .foregroundStyle(.secondary) + } + + Divider() + + // Right: token info + last updated + VStack(alignment: .leading, spacing: 6) { + if let tokens = entry.status.remainingTokens { + Label { + Text("\(tokens.formatted()) tokens") + .font(.caption) + } icon: { + Image(systemName: "bolt.fill") + .foregroundStyle(.orange) + } + } + + Label { + Text(entry.status.lastUpdated, style: .relative) + .font(.caption) + .foregroundStyle(.secondary) + } icon: { + Image(systemName: "clock") + .foregroundStyle(.secondary) + } + } + } + .padding(14) + } +} + +// MARK: - Accessory Views + +struct AccessoryCircularStatusView: View { + let entry: ClaudeStatusEntry + + var statusColor: Color { + switch entry.status.apiStatus { + case .operational: return .green + case .degraded: return .yellow + case .outage: return .red + } + } + + var body: some View { + ZStack { + Circle().fill(statusColor.opacity(0.2)) + Image(systemName: "cpu.fill") + .foregroundStyle(.purple) + } + } +} + +struct AccessoryRectangularStatusView: View { + let entry: ClaudeStatusEntry + + var body: some View { + HStack { + Image(systemName: "cpu.fill") + VStack(alignment: .leading) { + Text("Claude API") + .font(.headline) + Text(entry.status.apiStatus.rawValue) + .font(.caption) + } + } + } +} + +struct AccessoryInlineStatusView: View { + let entry: ClaudeStatusEntry + + var body: some View { + Label(entry.status.apiStatus.rawValue, systemImage: "cpu.fill") + } +} + +// MARK: - Preview + +#Preview(as: .systemSmall) { + ClaudeStatusWidget() +} timeline: { + ClaudeStatusEntry.placeholder +} + +#Preview(as: .systemMedium) { + ClaudeStatusWidget() +} timeline: { + ClaudeStatusEntry.placeholder +} diff --git a/examples/ios-widget/README.md b/examples/ios-widget/README.md new file mode 100644 index 0000000000..f7f2a98f48 --- /dev/null +++ b/examples/ios-widget/README.md @@ -0,0 +1,96 @@ +# Claude Code iOS 26 Widget + +A WidgetKit extension for iOS 26 that shows Claude Code session statistics and API status directly on the Home Screen and Lock Screen. + +## Widgets + +### Claude Status Widget +Shows the current Claude API status and active model. + +**Supported sizes:** Small, Medium, Accessory Circular, Accessory Rectangular, Accessory Inline + +| Small | Medium | Accessory Circular | +|-------|--------|--------------------| +| API status + model | Status + token count + last update | Status indicator dot | + +### Claude Session Stats Widget +Shows your Claude Code session statistics (sessions, files edited, tasks completed, lines changed). + +**Supported sizes:** Small, Medium, Large + +| Small | Medium | Large | +|-------|--------|-------| +| Session count + files + tasks | 4-stat grid | Full stats + model info | + +## Requirements + +- iOS 26+ +- Xcode 16+ +- Swift 6 +- WidgetKit framework + +## Project Structure + +``` +ClaudeWidget/ +├── ClaudeWidget.xcodeproj/ +│ └── project.pbxproj +└── WidgetExtension/ + ├── ClaudeWidgetBundle.swift # Widget entry point (@main) + ├── Info.plist + ├── Assets.xcassets/ + ├── Model/ + │ └── WidgetData.swift # Data models + App Group access + ├── Provider/ + │ ├── ClaudeStatusProvider.swift # Timeline provider (15 min refresh) + │ └── ClaudeSessionProvider.swift # Timeline provider (30 min refresh) + └── Views/ + ├── ClaudeStatusWidgetView.swift # Status widget views + └── ClaudeSessionWidgetView.swift # Session stats widget views +``` + +## Integration + +### Writing data from the main app + +The widget reads data from a shared **App Group** (`group.com.anthropic.claude-code.widget`). +Write session data from your main app target: + +```swift +import Foundation + +func saveSessionData(_ session: ClaudeSessionData) { + guard + let encoded = try? JSONEncoder().encode(session), + let defaults = UserDefaults(suiteName: "group.com.anthropic.claude-code.widget") + else { return } + defaults.set(encoded, forKey: "claudeSession") + WidgetCenter.shared.reloadTimelines(ofKind: "ClaudeSessionWidget") +} + +func saveStatusData(_ status: ClaudeStatusData) { + guard + let encoded = try? JSONEncoder().encode(status), + let defaults = UserDefaults(suiteName: "group.com.anthropic.claude-code.widget") + else { return } + defaults.set(encoded, forKey: "claudeStatus") + WidgetCenter.shared.reloadTimelines(ofKind: "ClaudeStatusWidget") +} +``` + +### Adding to your Xcode project + +1. Open your existing Claude Code app project in Xcode. +2. **File > New > Target** and choose **Widget Extension**. +3. Copy the source files from `WidgetExtension/` into the new target. +4. Enable the **App Groups** capability on both the app target and widget target, using the same group ID (`group.com.anthropic.claude-code.widget`). +5. Build and run on a device or simulator running iOS 26. + +## iOS 26 Features Used + +- `containerBackground(.fill.tertiary, for: .widget)` — required on iOS 17+ for widget backgrounds +- `@Environment(\.widgetFamily)` — adapts layout per widget size +- `.accessoryCircular` / `.accessoryRectangular` / `.accessoryInline` — Lock Screen widgets (iOS 16+) +- `.ultraThinMaterial` backgrounds on stat tiles — glass-morphism effect +- Swift 6 strict concurrency (`Sendable` data models via `Codable`) +- `#Preview(as:)` macro — live widget previews in Xcode 16