[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6E6AB8VTFWSYEM1WCAYEPBC":3,"skill-same:01M6E6AB8VTFWSYEM1WCAYEPBC":69},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":46,"commitSha":47,"skillDirectory":48,"forksCount":49,"repositoryHtmlUrl":50,"repositoryHomepageUrl":51,"ownerHtmlUrl":52,"ownerBlogUrl":6,"ownerDescription":53,"updatedAt":54,"install":55,"resources":68},"01M6E6AB8VTFWSYEM1WCAYEPBC","axiom-extensions-widgets",null,"Use when implementing widgets, Live Activities, or Control Center controls - enforces correct patterns for timeline management, data sharing, and extension lifecycle to prevent common crashes and memory issues","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",67,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002FCharlesWiltgen\u002FAxiom\u002Faxiom-extensions-widgets\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"axiom extensions widgets","widgets","management","controls","extension","live","control","patterns","live activities","use","memory","common","correct","lifecycle","timeline","sharing","data","ios","issues","2026-11-10 10:12:30",false,"---\nname: axiom-extensions-widgets\ndescription: Use when implementing widgets, Live Activities, or Control Center controls - enforces correct patterns for timeline management, data sharing, and extension lifecycle to prevent common crashes and memory issues\nlicense: MIT\ncompatibility: iOS 14+, iPadOS 14+, watchOS 9+\nmetadata:\n  version: \"1.0.0\"\n---\n\n# Extensions & Widgets — Discipline\n\n## Core Philosophy\n\n> \"Widgets are not mini apps. They're glanceable views into your app's data, rendered at strategic moments and displayed by the system. Extensions run in sandboxed environments with limited memory and execution time.\"\n\n**Mental model**: Think of widgets as **archived snapshots** on a timeline, not live views. Your widget doesn't \"run\" continuously — it renders, gets archived, and the system displays the snapshot.\n\n**Extension sandboxing**: Extensions have:\n- Limited memory (~30MB)\n- No network access in widget views (fetch in TimelineProvider only)\n- Separate bundle container from main app\n- Require App Groups for data sharing\n\n## When to Use This Skill\n\n✅ **Use this skill when**:\n- Implementing any widget (Home Screen, Lock Screen, StandBy, Control Center)\n- Creating Live Activities\n- Debugging why widgets show stale data\n- Widget not appearing in gallery\n- Interactive buttons not responding\n- Live Activity fails to start\n- Control Center control is unresponsive\n- Sharing data between app and widget\u002Fextension\n\n❌ **Do NOT use this skill for**:\n- Pure App Intents implementation (use **app-intents-ref**)\n- SwiftUI layout questions (use **swiftui-layout**)\n- Performance profiling (use **swiftui-performance**)\n- General debugging (use **xcode-debugging**)\n\n## Related Skills\n\n- **extensions-widgets-ref** — Comprehensive API reference\n- **app-intents-ref** — App Intents for interactive widgets\n- **swift-concurrency** — Async patterns for data fetching\n- **swiftdata** — Using SwiftData with App Groups\n\n## Example Prompts\n\n#### 1. \"My widget isn't updating\"\n→ This skill covers timeline policies, refresh budgets, manual reload, and App Groups configuration\n\n#### 2. \"How do I share data between app and widget?\"\n→ This skill explains App Groups entitlement, shared UserDefaults, and container URLs\n\n#### 3. \"Widget shows old data even after I update the app\"\n→ This skill covers container paths, UserDefaults suite names, and WidgetCenter reload\n\n#### 4. \"Live Activity fails to start\"\n→ This skill covers 4KB data limit, ActivityAttributes constraints, authorization checks\n\n#### 5. \"Control Center control takes forever to respond\"\n→ This skill covers async ValueProvider patterns and optimistic UI\n\n#### 6. \"Interactive widget button does nothing\"\n→ This skill covers App Intent perform() implementation and WidgetCenter reload\n\n---\n\n# Red Flags \u002F Anti-Patterns\n\n## Pattern 1: Network Calls in Widget View\n\n**Time cost**: 2-4 hours debugging why widgets are blank or show errors\n\n### Symptom\n- Widget renders but shows no data\n- Console errors: \"NSURLSession not available in widget extension\"\n- Widget appears blank intermittently\n\n### ❌ BAD Code\n\n```swift\nstruct MyWidgetView: View {\n    @State private var data: String?\n\n    var body: some View {\n        VStack {\n            if let data = data {\n                Text(data)\n            }\n        }\n        .onAppear {\n            \u002F\u002F ❌ WRONG — Network in widget view\n            Task {\n                let (data, _) = try await URLSession.shared.data(from: apiURL)\n                self.data = String(data: data, encoding: .utf8)\n            }\n        }\n    }\n}\n```\n\n**Why it fails**: Widget views are rendered, archived, and reused. Network calls in views are unreliable and may not execute.\n\n### ✅ GOOD Code\n\n```swift\n\u002F\u002F Main app — prefetch and save\nfunc updateWidgetData() async {\n    let data = try await fetchFromAPI()\n    let shared = UserDefaults(suiteName: \"group.com.myapp\")!\n    shared.set(data, forKey: \"widgetData\")\n\n    WidgetCenter.shared.reloadAllTimelines()\n}\n\n\u002F\u002F Widget TimelineProvider — read from shared storage\nstruct Provider: TimelineProvider {\n    func getTimeline(in context: Context, completion: @escaping (Timeline\u003CEntry>) -> ()) {\n        let shared = UserDefaults(suiteName: \"group.com.myapp\")!\n        let data = shared.string(forKey: \"widgetData\") ?? \"No data\"\n\n        let entry = SimpleEntry(date: Date(), data: data)\n        let timeline = Timeline(entries: [entry], policy: .atEnd)\n        completion(timeline)\n    }\n}\n```\n\n**Pattern**: Fetch data in main app, save to shared storage, read in widget.\n\n**Can TimelineProvider make network requests?**\n\nYes, but with important caveats:\n\n```swift\nstruct Provider: TimelineProvider {\n    func getTimeline(in context: Context, completion: @escaping (Timeline\u003CEntry>) -> ()) {\n        Task {\n            \u002F\u002F ✅ Network requests ARE allowed here\n            let data = try await fetchFromAPI()\n            let entry = SimpleEntry(date: Date(), data: data)\n            completion(Timeline(entries: [entry], policy: .atEnd))\n        }\n    }\n}\n```\n\n**Constraints**:\n- **30-second timeout** - System kills extension if getTimeline() doesn't complete\n- **No background sessions** - Can't download large files\n- **Battery cost** - Every timeline reload uses battery\n- **Not guaranteed** - May fail on poor connections\n\n**Best practice**: Prefetch in main app (faster, more reliable), use TimelineProvider network as fallback only.\n\n---\n\n## Pattern 2: Missing App Groups\n\n**Time cost**: 1-2 hours debugging why widget shows empty\u002Fdefault data\n\n### Symptom\n- Widget always shows placeholder or default values\n- Changes in main app don't reflect in widget\n- UserDefaults reads return nil in widget\n\n### ❌ BAD Code\n\n```swift\n\u002F\u002F Main app\nUserDefaults.standard.set(\"Updated\", forKey: \"myKey\")\n\n\u002F\u002F Widget extension\nlet value = UserDefaults.standard.string(forKey: \"myKey\") \u002F\u002F Returns nil!\n```\n\n**Why it fails**: `UserDefaults.standard` accesses different containers in app vs. extension.\n\n### ✅ GOOD Code\n\n```swift\n\u002F\u002F 1. Enable App Groups entitlement in BOTH targets:\n\u002F\u002F    - Main app target: Signing & Capabilities → + App Groups → \"group.com.myapp\"\n\u002F\u002F    - Widget extension target: Same group identifier\n\n\u002F\u002F 2. Main app\nlet shared = UserDefaults(suiteName: \"group.com.myapp\")!\nshared.set(\"Updated\", forKey: \"myKey\")\n\n\u002F\u002F 3. Widget extension\nlet shared = UserDefaults(suiteName: \"group.com.myapp\")!\nlet value = shared.string(forKey: \"myKey\") \u002F\u002F Returns \"Updated\"\n```\n\n**Verification**:\n```swift\nlet containerURL = FileManager.default.containerURL(\n    forSecurityApplicationGroupIdentifier: \"group.com.myapp\"\n)\nprint(\"Shared container: \\(containerURL?.path ?? \"MISSING\")\")\n\u002F\u002F Should print path, not \"MISSING\"\n```\n\n---\n\n## Pattern 3: Over-Refreshing (Budget Exhaustion)\n\n**Time cost**: Poor user experience, battery drain, widgets stop updating\n\n### Symptom\n- Widget updates frequently at first, then stops\n- Console logs: \"Timeline reload budget exhausted\"\n- Widget becomes stale after a few hours\n\n### ❌ BAD Code\n\n```swift\nfunc getTimeline(in context: Context, completion: @escaping (Timeline\u003CEntry>) -> ()) {\n    var entries: [SimpleEntry] = []\n\n    \u002F\u002F ❌ WRONG — 60 entries at 1-minute intervals\n    for minuteOffset in 0..\u003C60 {\n        let date = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: Date())!\n        entries.append(SimpleEntry(date: date, data: \"Data\"))\n    }\n\n    let timeline = Timeline(entries: entries, policy: .atEnd)\n    completion(timeline)\n}\n```\n\n**Why it's bad**: System gives 40-70 reloads\u002Fday. This approach uses 24 reloads\u002Fhour → exhausts budget in 2-3 hours.\n\n### ✅ GOOD Code\n\n```swift\nfunc getTimeline(in context: Context, completion: @escaping (Timeline\u003CEntry>) -> ()) {\n    var entries: [SimpleEntry] = []\n\n    \u002F\u002F ✅ CORRECT — 8 entries at 15-minute intervals (2 hours coverage)\n    for offset in 0..\u003C8 {\n        let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: Date())!\n        entries.append(SimpleEntry(date: date, data: getData()))\n    }\n\n    let timeline = Timeline(entries: entries, policy: .atEnd)\n    completion(timeline)\n}\n```\n\n**Guidelines**:\n- 15-60 minute intervals for most widgets\n- 5-15 minutes for time-sensitive data (stocks, sports)\n- Use `.atEnd` policy for automatic reload\n- Let system decide optimal refresh based on user engagement\n\n---\n\n## Pattern 4: Blocking Main Thread in Controls\n\n**Time cost**: Control Center control unresponsive, poor UX\n\n### Symptom\n- Tapping control in Control Center shows spinner for seconds\n- Control seems \"stuck\" or frozen\n- No immediate visual feedback\n\n### ❌ BAD Code\n\n```swift\nstruct ThermostatControl: ControlWidget {\n    var body: some ControlWidgetConfiguration {\n        StaticControlConfiguration(kind: \"Thermostat\") {\n            ControlWidgetButton(action: GetTemperatureIntent()) {\n                \u002F\u002F ❌ WRONG — Synchronous fetch blocks UI\n                let temp = HomeManager.shared.currentTemperature() \u002F\u002F Blocking call\n                Label(\"\\(temp)°\", systemImage: \"thermometer\")\n            }\n        }\n    }\n}\n```\n\n**Why it's bad**: Button renders on main thread. Blocking network\u002Fdatabase calls freeze UI.\n\n### ✅ GOOD Code\n\n```swift\nstruct ThermostatProvider: ControlValueProvider {\n    func currentValue() async throws -> ThermostatValue {\n        \u002F\u002F ✅ CORRECT — Async fetch, non-blocking\n        let temp = try await HomeManager.shared.fetchTemperature()\n        return ThermostatValue(temperature: temp)\n    }\n\n    var previewValue: ThermostatValue {\n        ThermostatValue(temperature: 72) \u002F\u002F Instant fallback\n    }\n}\n\nstruct ThermostatValue: ControlValueProviderValue {\n    var temperature: Int\n}\n\nstruct ThermostatControl: ControlWidget {\n    var body: some ControlWidgetConfiguration {\n        StaticControlConfiguration(kind: \"Thermostat\", provider: ThermostatProvider()) { value in\n            ControlWidgetButton(action: AdjustTemperatureIntent()) {\n                Label(\"\\(value.temperature)°\", systemImage: \"thermometer\")\n            }\n        }\n    }\n}\n```\n\n**Pattern**: Use `ControlValueProvider` for async data, provide instant `previewValue` fallback.\n\n---\n\n## Pattern 5: Missing Dismissal Policy (Zombie Live Activities)\n\n**Time cost**: User annoyance, negative reviews\n\n### Symptom\n- Live Activities stay on Lock Screen for hours after event ends\n- Users must manually dismiss completed activities\n- Activity shows \"Delivered\" but won't disappear\n\n### ❌ BAD Code\n\n```swift\n\u002F\u002F Start activity\nlet activity = try Activity.request(attributes: attributes, content: initialContent)\n\n\u002F\u002F Later... event completes\n\u002F\u002F ❌ WRONG — Never call .end()\n\u002F\u002F Activity stays forever until user dismisses\n```\n\n**Why it's bad**: Activities persist indefinitely unless explicitly ended.\n\n### ✅ GOOD Code\n\n```swift\n\u002F\u002F When event completes\nlet finalState = DeliveryAttributes.ContentState(\n    status: .delivered,\n    deliveredAt: Date()\n)\n\nawait activity.end(\n    ActivityContent(state: finalState, staleDate: nil),\n    dismissalPolicy: .default \u002F\u002F Removes after ~4 hours\n)\n\n\u002F\u002F Or for immediate removal\nawait activity.end(nil, dismissalPolicy: .immediate)\n\n\u002F\u002F Or remove at specific time\nlet dismissTime = Date().addingTimeInterval(30 * 60) \u002F\u002F 30 min\nawait activity.end(nil, dismissalPolicy: .after(dismissTime))\n```\n\n**Best practices**:\n- `.immediate` — Transient events (timer completed, song finished)\n- `.default` — Most activities (shows \"completed\" state for ~4 hours)\n- `.after(date)` — Specific end time (meeting ends, flight lands)\n\n---\n\n## Pattern 6: Exceeding 4KB Data Limit (Live Activities)\n\n**Time cost**: Activity fails to start silently, hard to debug\n\n### Symptom\n- `Activity.request()` throws error\n- Console: \"Activity attributes exceed size limit\"\n- Activity never appears on Lock Screen\n\n### ❌ BAD Code\n\n```swift\nstruct GameAttributes: ActivityAttributes {\n    struct ContentState: Codable, Hashable {\n        var teamALogo: Data \u002F\u002F ❌ Large image data\n        var teamBLogo: Data\n        var playByPlay: [String] \u002F\u002F ❌ Unbounded array\n        var statistics: [String: Any] \u002F\u002F ❌ Large dictionary\n    }\n\n    var gameID: String\n    var venueName: String\n}\n\n\u002F\u002F Fails if total size > 4KB\nlet activity = try Activity.request(attributes: attrs, content: content)\n```\n\n**Why it fails**: ActivityAttributes + ContentState combined must be \u003C 4KB.\n\n### ✅ GOOD Code\n\n```swift\nstruct GameAttributes: ActivityAttributes {\n    struct ContentState: Codable, Hashable {\n        var teamAScore: Int \u002F\u002F ✅ Small primitives\n        var teamBScore: Int\n        var quarter: Int\n        var timeRemaining: String \u002F\u002F \"2:34\"\n        var lastPlay: String? \u002F\u002F Single most recent play\n    }\n\n    var gameID: String \u002F\u002F ✅ Reference, not full data\n    var teamAName: String\n    var teamBName: String\n}\n\n\u002F\u002F Use asset catalog for images in view\nstruct GameLiveActivityView: View {\n    var context: ActivityViewContext\u003CGameAttributes>\n\n    var body: some View {\n        HStack {\n            Image(context.attributes.teamAName) \u002F\u002F Asset catalog\n            Text(\"\\(context.state.teamAScore)\")\n            \u002F\u002F ...\n        }\n    }\n}\n```\n\n**Strategies**:\n- Store IDs\u002Freferences, not full objects\n- Use asset catalogs for images (not embedded Data)\n- Keep ContentState minimal (only changeable data)\n- Use computed properties in views for derived data\n\n### Size Targets (Safety Margins)\n\n**Hard limit**: 4096 bytes (4KB)\n\n**Target guidance**:\n- ✅ **\u003C 2KB**: Safe with room to grow - recommended for v1.0\n- ⚠️ **2-3KB**: Acceptable but monitor closely as you add features\n- ? **3.5KB+**: Risky - future fields may push you over limit\n\n**Why safety margins matter**: You'll add fields later (new features, more data). Starting at 3.8KB leaves zero room for growth.\n\n**Checking size**:\n```swift\nlet attributes = GameAttributes(gameID: \"123\", teamAName: \"Hawks\", teamBName: \"Eagles\")\nlet state = GameAttributes.ContentState(teamAScore: 14, teamBScore: 10, quarter: 2, timeRemaining: \"5:23\", lastPlay: nil)\n\nlet encoder = JSONEncoder()\nif let attributesData = try? encoder.encode(attributes),\n   let stateData = try? encoder.encode(state) {\n    let totalSize = attributesData.count + stateData.count\n    print(\"Total size: \\(totalSize) bytes\")\n\n    if totalSize \u003C 2048 {\n        print(\"✅ Safe with room to grow\")\n    } else if totalSize \u003C 3072 {\n        print(\"⚠️ Acceptable but monitor\")\n    } else if totalSize \u003C 3584 {\n        print(\"? Risky - optimize now\")\n    } else {\n        print(\"❌ CRITICAL - will likely fail\")\n    }\n}\n```\n\n**Optimization priorities** (when over 2KB):\n1. Replace `String` descriptions with enums (if fixed set)\n2. Shorten string values (\"Team A\" → \"A\")\n3. Use smaller types (Int → Int8 if range allows)\n4. Remove optional fields that are rarely used\n\n---\n\n## Pattern 7: Widget Not Appearing in Gallery\n\n**Time cost**: 30 minutes debugging invisible widget\n\n### Symptom\n- Widget builds successfully\n- No errors in console\n- Widget doesn't appear in widget picker\u002Fgallery\n- Can't add to Home Screen\n\n### ❌ BAD Code\n\n```swift\n@main\nstruct MyWidget: Widget {\n    var body: some WidgetConfiguration {\n        StaticConfiguration(kind: \"MyWidget\", provider: Provider()) { entry in\n            MyWidgetView(entry: entry)\n        }\n        .configurationDisplayName(\"My Widget\")\n        .description(\"Shows data\")\n        \u002F\u002F ❌ MISSING: supportedFamilies() — widget won't appear!\n    }\n}\n```\n\n**Why it fails**: Without supportedFamilies(), system doesn't know which sizes to offer.\n\n### ✅ GOOD Code\n\n```swift\n@main\nstruct MyWidget: Widget {\n    var body: some WidgetConfiguration {\n        StaticConfiguration(kind: \"MyWidget\", provider: Provider()) { entry in\n            MyWidgetView(entry: entry)\n        }\n        .configurationDisplayName(\"My Widget\")\n        .description(\"Shows data\")\n        .supportedFamilies([.systemSmall, .systemMedium]) \u002F\u002F ✅ Required\n    }\n}\n```\n\n**Other common causes**:\n- Widget target's \"Skip Install\" set to YES (should be NO)\n- Widget extension not added to app's \"Embed App Extensions\"\n- Clean build folder needed (`Cmd+Shift+K`)\n\n---\n\n# Decision Tree\n\n```\nWidget\u002FExtension Issue?\n│\n├─ Widget not appearing in gallery?\n│  ├─ Check WidgetBundle registered in @main\n│  ├─ Verify supportedFamilies() includes intended families\n│  └─ Clean build folder, restart Xcode\n│\n├─ Widget not refreshing?\n│  ├─ Timeline policy set to .never?\n│  │  └─ Change to .atEnd or .after(date)\n│  ├─ Budget exhausted? (too frequent reloads)\n│  │  └─ Increase interval between entries (15-60 min)\n│  └─ Manual reload\n│     └─ WidgetCenter.shared.reloadAllTimelines()\n│\n├─ Widget shows empty\u002Fold data?\n│  ├─ App Groups configured in BOTH targets?\n│  │  ├─ No → Add \"App Groups\" entitlement\n│  │  └─ Yes → Verify same group ID\n│  ├─ Using UserDefaults.standard?\n│  │  └─ Change to UserDefaults(suiteName: \"group.com.myapp\")\n│  └─ Shared container path correct?\n│     └─ Print containerURL, verify not nil\n│\n├─ Interactive button not working?\n│  ├─ App Intent perform() returns value?\n│  │  └─ Must return IntentResult\n│  ├─ perform() updates shared data?\n│  │  └─ Update App Group storage\n│  └─ Calls WidgetCenter.reloadTimelines()?\n│     └─ Reload to reflect changes\n│\n├─ Live Activity fails to start?\n│  ├─ Data size > 4KB?\n│  │  └─ Reduce ActivityAttributes + ContentState\n│  ├─ Authorization enabled?\n│  │  └─ Check ActivityAuthorizationInfo().areActivitiesEnabled\n│  └─ pushType correct?\n│     └─ nil for local updates, .token for push\n│\n├─ Control Center control unresponsive?\n│  ├─ Async operation blocking UI?\n│  │  └─ Use ControlValueProvider with async currentValue()\n│  └─ Provide previewValue for instant fallback\n│\n└─ watchOS Live Activity not showing?\n   ├─ supplementalActivityFamilies includes .small?\n   └─ Apple Watch paired and in range?\n```\n\n---\n\n# Mandatory First Steps\n\nBefore debugging any widget or extension issue, complete this checklist:\n\n## Widget Debugging Checklist\n\n- ☐ **App Groups enabled** in BOTH main app AND extension targets\n  ```bash\n  # Verify entitlements\n  codesign -d --entitlements - \u002Fpath\u002Fto\u002FYourApp.app\n  # Should show com.apple.security.application-groups\n  ```\n\n- ☐ **Widget in Widget Gallery** (not just on Home Screen)\n  - Long-press Home Screen → + button → Find your widget\n  - Verify it appears with correct name and description\n\n- ☐ **Console logs** for timeline errors\n  ```bash\n  # Xcode Console\n  # Filter: \"widget\" OR \"timeline\"\n  # Look for: \"Timeline reload failed\", \"Budget exhausted\"\n  ```\n\n- ☐ **Manual reload test**\n  ```swift\n  WidgetCenter.shared.reloadAllTimelines()\n  ```\n  - If this fixes it → problem is timeline policy or refresh budget\n\n- ☐ **Shared container accessible**\n  ```swift\n  let container = FileManager.default.containerURL(\n      forSecurityApplicationGroupIdentifier: \"group.com.myapp\"\n  )\n  print(\"Container: \\(container?.path ?? \"NIL\")\")\n  \u002F\u002F Must print valid path, not \"NIL\"\n  ```\n\n## Live Activity Debugging Checklist\n\n- ☐ **ActivityAttributes \u003C 4KB**\n  ```swift\n  let encoded = try JSONEncoder().encode(attributes)\n  print(\"Size: \\(encoded.count) bytes\") \u002F\u002F Must be \u003C 4096\n  ```\n\n- ☐ **Authorization check**\n  ```swift\n  let authInfo = ActivityAuthorizationInfo()\n  print(\"Enabled: \\(authInfo.areActivitiesEnabled)\")\n  ```\n\n- ☐ **pushType matches server integration**\n  - `nil` → local updates only\n  - `.token` → expects push notifications\n\n- ☐ **Dismissal policy implemented**\n  - Every activity.end() must specify policy\n\n## Control Center Widget Checklist\n\n- ☐ **ControlValueProvider for async data**\n- ☐ **previewValue provides instant fallback**\n- ☐ **App Intent perform() is async**\n- ☐ **No blocking network\u002Fdatabase calls in views**\n\n---\n\n# Pressure Scenarios\n\n## Scenario 1: \"Widget shows wrong data in production\"\n\n### Situation\n- App released to App Store\n- Users report widget displaying incorrect\u002Fstale information\n- Works fine in development\n\n### Pressure Signals\n- ? **App Store reviews** — 1-star reviews mentioning broken widget\n- ⏰ **Time pressure** — Need hotfix ASAP\n- ? **Executive visibility** — Management asking for status updates\n\n### Rationalization Traps (DO NOT)\n\n1. *\"Just force a timeline reload more often\"*\n   - **Why it fails**: Exhausts budget, makes problem worse\n\n2. *\"The widget worked in testing\"*\n   - **Why it fails**: Development vs. production App Groups mismatch\n\n3. *\"Users should just restart their phone\"*\n   - **Why it fails**: Not a fix, damages reputation\n\n### MANDATORY Systematic Fix\n\n#### Step 1: Verify App Groups (30 min)\n\n```swift\n\u002F\u002F Add logging to BOTH app and widget\nlet group = \"group.com.myapp.production\" \u002F\u002F Must match exactly\nlet container = FileManager.default.containerURL(\n    forSecurityApplicationGroupIdentifier: group\n)\n\nprint(\"[\\(Bundle.main.bundleIdentifier ?? \"?\")] Container: \\(container?.path ?? \"NIL\")\")\n\n\u002F\u002F Log EVERY read\u002Fwrite\nlet shared = UserDefaults(suiteName: group)!\nprint(\"Writing key 'lastUpdate' = \\(Date())\")\nshared.set(Date(), forKey: \"lastUpdate\")\n```\n\n**Verify**: Run app, then widget. Both should print SAME container path.\n\n#### Step 2: Check Container Paths\n\n```bash\n# Device logs (Xcode → Window → Devices and Simulators → View Device Logs)\n# Filter: Your app bundle ID\n# Look for: Container path mismatches\n```\n\nCommon issues:\n- App uses `group.com.myapp.dev`\n- Widget uses `group.com.myapp.production`\n- **Fix**: Ensure EXACT same group ID in both .entitlements files\n\n#### Step 3: Add Version Stamp\n\n```swift\n\u002F\u002F Main app — stamp every write\nstruct WidgetData: Codable {\n    var value: String\n    var timestamp: Date\n    var appVersion: String\n}\n\nlet data = WidgetData(\n    value: \"Latest\",\n    timestamp: Date(),\n    appVersion: Bundle.main.appVersion\n)\nshared.set(try JSONEncoder().encode(data), forKey: \"widgetData\")\n\n\u002F\u002F Widget — verify version\nif let data = shared.data(forKey: \"widgetData\"),\n   let decoded = try? JSONDecoder().decode(WidgetData.self, from: data) {\n    print(\"Widget reading data from app version: \\(decoded.appVersion)\")\n}\n```\n\n#### Step 4: Force Reload on App Launch\n\n```swift\n\u002F\u002F AppDelegate \u002F @main App\nfunc applicationDidBecomeActive(_ application: UIApplication) {\n    WidgetCenter.shared.reloadAllTimelines()\n}\n```\n\n### Communication Template\n\n**To stakeholders**:\n```\nStatus: Investigating widget data sync issue\n\nRoot cause: App Groups configuration mismatch between app and widget extension in production build\n\nFix: Updated both targets to use identical group identifier, added logging to prevent recurrence\n\nTimeline: Hotfix submitted to App Store review (24-48h)\n\nWorkaround for users: Force-quit app and relaunch (triggers widget refresh)\n```\n\n### Time Saved\n- **Without systematic fix**: 4-8 hours of trial-and-error, multiple resubmissions\n- **With this process**: 1-2 hours to identify, fix, and verify\n\n---\n\n## Scenario 2: \"Live Activity must update instantly\"\n\n### Situation\n- Sports score app\n- Users expect scores to update within seconds of real game events\n- Current timeline-based approach too slow\n\n### Pressure\n- **Competitive**: \"Other apps update faster\"\n- **Deadline**: Marketing promised \"real-time\" updates\n\n### Rationalization Traps (DO NOT)\n\n1. *\"Just create entries every 5 seconds\"*\n   - **Why it fails**: Not real-time, exhausts battery, doesn't scale\n\n2. *\"Add WebSocket to widget view\"*\n   - **Why it fails**: Extensions can't maintain persistent connections\n\n3. *\"Lower refresh interval to 1 second\"*\n   - **Why it fails**: Timeline system not designed for sub-minute updates\n\n### MANDATORY Solution: Phased Approach\n\n**Critical reality check**: Push notification entitlement approval takes **3-7 days**. Never promise features before approval.\n\n#### Phase 1: Ship with Local Updates (No Approval Required)\n\n**Ship immediately** with app-driven updates:\n\n```swift\n\u002F\u002F Start activity WITHOUT push (no entitlement needed)\nlet activity = try Activity.request(\n    attributes: attributes,\n    content: initialContent,\n    pushType: nil  \u002F\u002F Local updates only\n)\n\n\u002F\u002F In your app when data changes (user opens app, pulls to refresh)\nawait activity.update(ActivityContent(\n    state: updatedState,\n    staleDate: nil\n))\n```\n\n**Set expectations**: Updates occur when user interacts with app. This is **acceptable** for v1.0 and requires zero approval.\n\n#### Phase 2: Add Push After Approval (3-7 Days)\n\n**After entitlement approved**, switch to push:\n\n#### Step 1: Enable Push for Live Activities\n\n```swift\n\u002F\u002F 1. Entitlement: \"com.apple.developer.activity-push-notification\"\n\n\u002F\u002F 2. Request activity with push token\nlet activity = try Activity.request(\n    attributes: attributes,\n    content: initialContent,\n    pushType: .token\n)\n\n\u002F\u002F 3. Monitor for token\nTask {\n    for await pushToken in activity.pushTokenUpdates {\n        let tokenString = pushToken.map { String(format: \"%02x\", $0) }.joined()\n        await sendTokenToServer(activityID: activity.id, token: tokenString)\n    }\n}\n```\n\n#### Step 2: Server-Side Push (Phase 2 Only)\n\n```json\n{\n  \"aps\": {\n    \"timestamp\": 1633046400,\n    \"event\": \"update\",\n    \"content-state\": {\n      \"teamAScore\": 14,\n      \"teamBScore\": 10,\n      \"quarter\": 2,\n      \"timeRemaining\": \"5:23\"\n    },\n    \"alert\": {\n      \"title\": \"Touchdown!\",\n      \"body\": \"Team A scores\"\n    }\n  }\n}\n```\n\n**Standard push limit**: ~10-12 per hour\n\n#### Step 3: Request Frequent Updates Entitlement (Phase 2, iOS 18.2+)\n\nFor apps requiring more frequent pushes (sports, stocks):\n\n```xml\n\u003Ckey>com.apple.developer.activity-push-notification-frequent-updates\u003C\u002Fkey>\n\u003Ctrue\u002F>\n```\n\n**Requires justification** in App Store Connect: \"Live sports scores require immediate updates for user engagement\"\n\n#### Verification\n\n```swift\n\u002F\u002F Log push receipt in Live Activity widget\n#if DEBUG\nlet logURL = FileManager.default.containerURL(\n    forSecurityApplicationGroupIdentifier: \"group.com.myapp\"\n)!.appendingPathComponent(\"push_log.txt\")\n\nlet timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)\ntry! \"\\(timestamp): Received push\\n\".append(to: logURL)\n#endif\n```\n\n### Communication Template\n\n**To marketing\u002Fexec (Phase 1)**:\n```\nLaunch Timeline:\n- Phase 1 (immediate): Live Activities with app-driven updates. Updates appear when users open app or pull to refresh.\n- Phase 2 (3-7 days): Push notification integration after Apple approval. Updates arrive within 1-3 seconds of server events.\n\nRecommendation: Launch Phase 1 to market, communicate Phase 2 as \"coming soon\" once approved.\n```\n\n**To marketing\u002Fexec (Phase 2)**:\n```\n\"Real-time\" positioning requires clarification:\n\nTechnical: Live Activities update via push notifications with 1-3 second latency from server to device\n\nConstraints: Apple's push system has rate limits (~10\u002Fhour standard, axiom-higher with special entitlement)\n\nCompetitive analysis: Competitors likely use same system with similar limitations\n\nRecommendation: Position as \"near real-time\" (accurate) vs \"instant\" (misleading)\n```\n\n### Reality Check\n- Push notifications are fastest mechanism available\n- 1-3 second latency is normal\n- Budget limits exist for battery optimization\n- Users prefer longer battery life over millisecond-faster scores\n\n> For comprehensive push notification setup (APNs auth, payload format, token management, service extensions), see axiom-push-notifications and axiom-push-notifications-ref. This skill covers the ActivityKit UI and state management side.\n\n---\n\n## Scenario 3: \"Control Center control is slow\"\n\n### Situation\n- Smart home control for lights\n- Tapping control in Control Center takes 3-5 seconds to respond\n- Users expect instant feedback\n\n### MANDATORY Fix: Optimistic UI + Async Value Provider\n\n#### Problem Code\n\n```swift\nstruct LightControl: ControlWidget {\n    var body: some ControlWidgetConfiguration {\n        StaticControlConfiguration(kind: \"Light\") {\n            ControlWidgetToggle(\n                isOn: LightManager.shared.isOn, \u002F\u002F ❌ Blocking fetch\n                action: ToggleLightIntent()\n            ) { isOn in\n                Label(isOn ? \"On\" : \"Off\", systemImage: \"lightbulb.fill\")\n            }\n        }\n    }\n}\n```\n\n#### Fixed Code\n\n```swift\n\u002F\u002F 1. Value Provider for async state\nstruct LightProvider: ControlValueProvider {\n    func currentValue() async throws -> LightValue {\n        \u002F\u002F Async fetch from HomeKit\u002Fserver\n        let isOn = try await HomeManager.shared.fetchLightState()\n        return LightValue(isOn: isOn)\n    }\n\n    var previewValue: LightValue {\n        \u002F\u002F Instant fallback from cache\n        let shared = UserDefaults(suiteName: \"group.com.myapp\")!\n        return LightValue(isOn: shared.bool(forKey: \"lastKnownLightState\"))\n    }\n}\n\nstruct LightValue: ControlValueProviderValue {\n    var isOn: Bool\n}\n\n\u002F\u002F 2. Optimistic Intent\nstruct ToggleLightIntent: AppIntent {\n    static var title: LocalizedStringResource = \"Toggle Light\"\n\n    func perform() async throws -> some IntentResult {\n        \u002F\u002F Immediately update cache (optimistic)\n        let shared = UserDefaults(suiteName: \"group.com.myapp\")!\n        let currentState = shared.bool(forKey: \"lastKnownLightState\")\n        let newState = !currentState\n        shared.set(newState, forKey: \"lastKnownLightState\")\n\n        \u002F\u002F Then update actual device (async)\n        try await HomeManager.shared.setLight(isOn: newState)\n\n        return .result()\n    }\n}\n\n\u002F\u002F 3. Control with provider\nstruct LightControl: ControlWidget {\n    var body: some ControlWidgetConfiguration {\n        StaticControlConfiguration(kind: \"Light\", provider: LightProvider()) { value in\n            ControlWidgetToggle(\n                isOn: value.isOn,\n                action: ToggleLightIntent()\n            ) { isOn in\n                Label(isOn ? \"On\" : \"Off\", systemImage: \"lightbulb.fill\")\n                    .tint(isOn ? .yellow : .gray)\n            }\n        }\n    }\n}\n```\n\n**Result**: Control responds instantly with cached state, actual device updates in background.\n\n---\n\n# Final Checklist\n\nBefore shipping widgets or Live Activities:\n\n## Pre-Release\n- ☐ App Groups entitlement in BOTH targets (app + extension)\n- ☐ Shared UserDefaults uses `suiteName` (not `.standard`)\n- ☐ Timeline entries ≥ 5 minutes apart (avoid budget exhaustion)\n- ☐ No network calls in widget views (only in TimelineProvider)\n- ☐ ActivityAttributes + ContentState \u003C 4KB\n- ☐ Live Activities call `.end()` with appropriate dismissal policy\n- ☐ Control Center controls use ControlValueProvider for async data\n- ☐ Tested on actual device (not just simulator) — **Required because**:\n  - Simulator doesn't enforce timeline budget limits\n  - Push notifications don't work in simulator\n  - App Groups container paths differ (simulator vs device)\n  - Memory limits not enforced in simulator\n  - Background refresh behavior different\n- ☐ Tested all supported widget families\n- ☐ Verified widget appears in Widget Gallery\n\n## Post-Release Monitoring\n- ☐ Monitor for \"Timeline reload budget exhausted\" errors\n- ☐ Track widget data staleness in analytics\n- ☐ Watch App Store reviews for widget-related complaints\n- ☐ Log App Group container access for debugging\n\n## Common Failure Modes\n- Missing App Groups → Widget shows default data\n- Wrong group ID → App and widget can't communicate\n- Over-refreshing → Widget stops updating after hours\n- Network in view → Widget renders blank\n- No dismissal policy → Zombie Live Activities\n- Blocking main thread → Unresponsive controls\n\n---\n\n**Remember**: Widgets are NOT mini apps. They're glanceable snapshots rendered by the system. Extensions run in sandboxed environments with strict resource limits. Follow the patterns in this skill to avoid the most common pitfalls.\n\n---\n\n## Resources\n\n**Skills**: axiom-extensions-widgets-ref, axiom-push-notifications, axiom-push-notifications-ref, axiom-background-processing\n\n","# Extensions & Widgets — Discipline\n\n## Core Philosophy\n\n> \"Widgets are not mini apps. They're glanceable views into your app's data, rendered at strategic moments and displayed by the system. Extensions run in sandboxed environments with limited memory and execution time.\"\n\n**Mental model**: Think of widgets as **archived snapshots** on a timeline, not live views. Your widget doesn't \"run\" continuously — it renders, gets archived, and the system displays the snapshot.\n\n**Extension sandboxing**: Extensions have:\n- Limited memory (~30MB)\n- No network access in widget views (fetch in TimelineProvider only)\n- Separate bundle container from main app\n- Require App Groups for data sharing\n\n## When to Use This Skill\n\n✅ **Use this skill when**:\n- Implementing any widget (Home Screen, Lock Screen, StandBy, Control Center)\n- Creating Live Activities\n- Debugging why widgets show stale data\n- Widget not appearing in gallery\n- Interactive buttons not responding\n- Live Activity fails to start\n- Control Center control is unresponsive\n- Sharing data between app and widget\u002Fextension\n\n❌ **Do NOT use this skill for**:\n- Pure App Intents implementation (use **app-intents-ref**)\n- SwiftUI layout questions (use **swiftui-layout**)\n- Performance profiling (use **swiftui-performance**)\n- General debugging (use **xcode-debugging**)\n\n## Related Skills\n\n- **extensions-widgets-ref** — Comprehensive API reference\n- **app-intents-ref** — App Intents for interactive widgets\n- **swift-concurrency** — Async patterns for data fetching\n- **swiftdata** — Using SwiftData with App Groups\n\n## Example Prompts\n\n#### 1. \"My widget isn't updating\"\n→ This skill covers timeline policies, refresh budgets, manual reload, and App Groups configuration\n\n#### 2. \"How do I share data between app and widget?\"\n→ This skill explains App Groups entitlement, shared UserDefaults, and container URLs\n\n#### 3. \"Widget shows old data even after I update the app\"\n→ This skill covers container paths, UserDefaults suite names, and WidgetCenter reload\n\n#### 4. \"Live Activity fails to start\"\n→ This skill covers 4KB data limit, ActivityAttributes constraints, authorization checks\n\n#### 5. \"Control Center control takes forever to respond\"\n→ This skill covers async ValueProvider patterns and optimistic UI\n\n#### 6. \"Interactive widget button does nothing\"\n→ This skill covers App Intent perform() implementation and WidgetCenter reload\n\n---\n\n# Red Flags \u002F Anti-Patterns\n\n## Pattern 1: Network Calls in Widget View\n\n**Time cost**: 2-4 hours debugging why widgets are blank or show errors\n\n### Symptom\n- Widget renders but shows no data\n- Console errors: \"NSURLSession not available in widget extension\"\n- Widget appears blank intermittently\n\n### ❌ BAD Code\n\n```swift\nstruct MyWidgetView: View {\n    @State private var data: String?\n\n    var body: some View {\n        VStack {\n            if let data = data {\n                Text(data)\n            }\n        }\n        .onAppear {\n            \u002F\u002F ❌ WRONG — Network in widget view\n            Task {\n                let (data, _) = try await URLSession.shared.data(from: apiURL)\n                self.data = String(data: data, encoding: .utf8)\n            }\n        }\n    }\n}\n```\n\n**Why it fails**: Widget views are rendered, archived, and reused. Network calls in views are unreliable and may not execute.\n\n### ✅ GOOD Code\n\n```swift\n\u002F\u002F Main app — prefetch and save\nfunc updateWidgetData() async {\n    let data = try await fetchFromAPI()\n    let shared = UserDefaults(suiteName: \"group.com.myapp\")!\n    shared.set(data, forKey: \"widgetData\")\n\n    WidgetCenter.shared.reloadAllTimelines()\n}\n\n\u002F\u002F Widget TimelineProvider — read from shared storage\nstruct Provider: TimelineProvider {\n    func getTimeline(in context: Context, completion: @escaping (Timeline\u003CEntry>) -> ()) {\n        let shared = UserDefaults(suiteName: \"group.com.myapp\")!\n        let data = shared.string(forKey: \"widgetData\") ?? \"No data\"\n\n        let entry = SimpleEntry(date: Date(), data: data)\n        let timeline = Timeline(entries: [entry], policy: .atEnd)\n        completion(timeline)\n    }\n}\n```\n\n**Pattern**: Fetch data in main app, save to shared storage, read in widget.\n\n**Can TimelineProvider make network requests?**\n\nYes, but with important caveats:\n\n```swift\nstruct Provider: TimelineProvider {\n    func getTimeline(in context: Context, completion: @escaping (Timeline\u003CEntry>) -> ()) {\n        Task {\n            \u002F\u002F ✅ Network requests ARE allowed here\n            let data = try await fetchFromAPI()\n            let entry = SimpleEntry(date: Date(), data: data)\n            completion(Timeline(entries: [entry], policy: .atEnd))\n        }\n    }\n}\n```\n\n**Constraints**:\n- **30-second timeout** - System kills extension if getTimeline() doesn't complete\n- **No background sessions** - Can't download large files\n- **Battery cost** - Every timeline reload uses battery\n- **Not guaranteed** - May fail on poor connections\n\n**Best practice**: Prefetch in main app (faster, more reliable), use TimelineProvider network as fallback only.\n\n---\n\n## Pattern 2: Missing App Groups\n\n**Time cost**: 1-2 hours debugging why widget shows empty\u002Fdefault data\n\n### Symptom\n- Widget always shows placeholder or default values\n- Changes in main app don't reflect in widget\n- UserDefaults reads return nil in widget\n\n### ❌ BAD Code\n\n```swift\n\u002F\u002F Main app\nUserDefaults.standard.set(\"Updated\", forKey: \"myKey\")\n\n\u002F\u002F Widget extension\nlet value = UserDefaults.standard.string(forKey: \"myKey\") \u002F\u002F Returns nil!\n```\n\n**Why it fails**: `UserDefaults.standard` accesses different containers in app vs. extension.\n\n### ✅ GOOD Code\n\n```swift\n\u002F\u002F 1. Enable App Groups entitlement in BOTH targets:\n\u002F\u002F    - Main app target: Signing & Capabilities → + App Groups → \"group.com.myapp\"\n\u002F\u002F    - Widget extension target: Same group identifier\n\n\u002F\u002F 2. Main app\nlet shared = UserDefaults(suiteName: \"group.com.myapp\")!\nshared.set(\"Updated\", forKey: \"myKey\")\n\n\u002F\u002F 3. Widget extension\nlet shared = UserDefaults(suiteName: \"group.com.myapp\")!\nlet value = shared.string(forKey: \"myKey\") \u002F\u002F Returns \"Updated\"\n```\n\n**Verification**:\n```swift\nlet containerURL = FileManager.default.containerURL(\n    forSecurityApplicationGroupIdentifier: \"group.com.myapp\"\n)\nprint(\"Shared container: \\(containerURL?.path ?? \"MISSING\")\")\n\u002F\u002F Should print path, not \"MISSING\"\n```\n\n---\n\n## Pattern 3: Over-Refreshing (Budget Exhaustion)\n\n**Time cost**: Poor user experience, battery drain, widgets stop updating\n\n### Symptom\n- Widget updates frequently at first, then stops\n- Console logs: \"Timeline reload budget exhausted\"\n- Widget becomes stale after a few hours\n\n### ❌ BAD Code\n\n```swift\nfunc getTimeline(in context: Context, completion: @escaping (Timeline\u003CEntry>) -> ()) {\n    var entries: [SimpleEntry] = []\n\n    \u002F\u002F ❌ WRONG — 60 entries at 1-minute intervals\n    for minuteOffset in 0..\u003C60 {\n        let date = Calendar.current.date(byAdding: .minute, value: minuteOffset, to: Date())!\n        entries.append(SimpleEntry(date: date, data: \"Data\"))\n    }\n\n    let timeline = Timeline(entries: entries, policy: .atEnd)\n    completion(timeline)\n}\n```\n\n**Why it's bad**: System gives 40-70 reloads\u002Fday. This approach uses 24 reloads\u002Fhour → exhausts budget in 2-3 hours.\n\n### ✅ GOOD Code\n\n```swift\nfunc getTimeline(in context: Context, completion: @escaping (Timeline\u003CEntry>) -> ()) {\n    var entries: [SimpleEntry] = []\n\n    \u002F\u002F ✅ CORRECT — 8 entries at 15-minute intervals (2 hours coverage)\n    for offset in 0..\u003C8 {\n        let date = Calendar.current.date(byAdding: .minute, value: offset * 15, to: Date())!\n        entries.append(SimpleEntry(date: date, data: getData()))\n    }\n\n    let timeline = Timeline(entries: entries, policy: .atEnd)\n    completion(timeline)\n}\n```\n\n**Guidelines**:\n- 15-60 minute intervals for most widgets\n- 5-15 minutes for time-sensitive data (stocks, sports)\n- Use `.atEnd` policy for automatic reload\n- Let system decide optimal refresh based on user engagement\n\n---\n\n## Pattern 4: Blocking Main Thread in Controls\n\n**Time cost**: Control Center control unresponsive, poor UX\n\n### Symptom\n- Tapping control in Control Center shows spinner for seconds\n- Control seems \"stuck\" or frozen\n- No immediate visual feedback\n\n### ❌ BAD Code\n\n```swift\nstruct ThermostatControl: ControlWidget {\n    var body: some ControlWidgetConfiguration {\n        StaticControlConfiguration(kind: \"Thermostat\") {\n            ControlWidgetButton(action: GetTemperatureIntent()) {\n                \u002F\u002F ❌ WRONG — Synchronous fetch blocks UI\n                let temp = HomeManager.shared.currentTemperature() \u002F\u002F Blocking call\n                Label(\"\\(temp)°\", systemImage: \"thermometer\")\n            }\n        }\n    }\n}\n```\n\n**Why it's bad**: Button renders on main thread. Blocking network\u002Fdatabase calls freeze UI.\n\n### ✅ GOOD Code\n\n```swift\nstruct ThermostatProvider: ControlValueProvider {\n    func currentValue() async throws -> ThermostatValue {\n        \u002F\u002F ✅ CORRECT — Async fetch, non-blocking\n        let temp = try await HomeManager.shared.fetchTemperature()\n        return ThermostatValue(temperature: temp)\n    }\n\n    var previewValue: ThermostatValue {\n        ThermostatValue(temperature: 72) \u002F\u002F Instant fallback\n    }\n}\n\nstruct ThermostatValue: ControlValueProviderValue {\n    var temperature: Int\n}\n\nstruct ThermostatControl: ControlWidget {\n    var body: some ControlWidgetConfiguration {\n        StaticControlConfiguration(kind: \"Thermostat\", provider: ThermostatProvider()) { value in\n            ControlWidgetButton(action: AdjustTemperatureIntent()) {\n                Label(\"\\(value.temperature)°\", systemImage: \"thermometer\")\n            }\n        }\n    }\n}\n```\n\n**Pattern**: Use `ControlValueProvider` for async data, provide instant `previewValue` fallback.\n\n---\n\n## Pattern 5: Missing Dismissal Policy (Zombie Live Activities)\n\n**Time cost**: User annoyance, negative reviews\n\n### Symptom\n- Live Activities stay on Lock Screen for hours after event ends\n- Users must manually dismiss completed activities\n- Activity shows \"Delivered\" but won't disappear\n\n### ❌ BAD Code\n\n```swift\n\u002F\u002F Start activity\nlet activity = try Activity.request(attributes: attributes, content: initialContent)\n\n\u002F\u002F Later... event completes\n\u002F\u002F ❌ WRONG — Never call .end()\n\u002F\u002F Activity stays forever until user dismisses\n```\n\n**Why it's bad**: Activities persist indefinitely unless explicitly ended.\n\n### ✅ GOOD Code\n\n```swift\n\u002F\u002F When event completes\nlet finalState = DeliveryAttributes.ContentState(\n    status: .delivered,\n    deliveredAt: Date()\n)\n\nawait activity.end(\n    ActivityContent(state: finalState, staleDate: nil),\n    dismissalPolicy: .default \u002F\u002F Removes after ~4 hours\n)\n\n\u002F\u002F Or for immediate removal\nawait activity.end(nil, dismissalPolicy: .immediate)\n\n\u002F\u002F Or remove at specific time\nlet dismissTime = Date().addingTimeInterval(30 * 60) \u002F\u002F 30 min\nawait activity.end(nil, dismissalPolicy: .after(dismissTime))\n```\n\n**Best practices**:\n- `.immediate` — Transient events (timer completed, song finished)\n- `.default` — Most activities (shows \"completed\" state for ~4 hours)\n- `.after(date)` — Specific end time (meeting ends, flight lands)\n\n---\n\n## Pattern 6: Exceeding 4KB Data Limit (Live Activities)\n\n**Time cost**: Activity fails to start silently, hard to debug\n\n### Symptom\n- `Activity.request()` throws error\n- Console: \"Activity attributes exceed size limit\"\n- Activity never appears on Lock Screen\n\n### ❌ BAD Code\n\n```swift\nstruct GameAttributes: ActivityAttributes {\n    struct ContentState: Codable, Hashable {\n        var teamALogo: Data \u002F\u002F ❌ Large image data\n        var teamBLogo: Data\n        var playByPlay: [String] \u002F\u002F ❌ Unbounded array\n        var statistics: [String: Any] \u002F\u002F ❌ Large dictionary\n    }\n\n    var gameID: String\n    var venueName: String\n}\n\n\u002F\u002F Fails if total size > 4KB\nlet activity = try Activity.request(attributes: attrs, content: content)\n```\n\n**Why it fails**: ActivityAttributes + ContentState combined must be \u003C 4KB.\n\n### ✅ GOOD Code\n\n```swift\nstruct GameAttributes: ActivityAttributes {\n    struct ContentState: Codable, Hashable {\n        var teamAScore: Int \u002F\u002F ✅ Small primitives\n        var teamBScore: Int\n        var quarter: Int\n        var timeRemaining: String \u002F\u002F \"2:34\"\n        var lastPlay: String? \u002F\u002F Single most recent play\n    }\n\n    var gameID: String \u002F\u002F ✅ Reference, not full data\n    var teamAName: String\n    var teamBName: String\n}\n\n\u002F\u002F Use asset catalog for images in view\nstruct GameLiveActivityView: View {\n    var context: ActivityViewContext\u003CGameAttributes>\n\n    var body: some View {\n        HStack {\n            Image(context.attributes.teamAName) \u002F\u002F Asset catalog\n            Text(\"\\(context.state.teamAScore)\")\n            \u002F\u002F ...\n        }\n    }\n}\n```\n\n**Strategies**:\n- Store IDs\u002Freferences, not full objects\n- Use asset catalogs for images (not embedded Data)\n- Keep ContentState minimal (only changeable data)\n- Use computed properties in views for derived data\n\n### Size Targets (Safety Margins)\n\n**Hard limit**: 4096 bytes (4KB)\n\n**Target guidance**:\n- ✅ **\u003C 2KB**: Safe with room to grow - recommended for v1.0\n- ⚠️ **2-3KB**: Acceptable but monitor closely as you add features\n- ? **3.5KB+**: Risky - future fields may push you over limit\n\n**Why safety margins matter**: You'll add fields later (new features, more data). Starting at 3.8KB leaves zero room for growth.\n\n**Checking size**:\n```swift\nlet attributes = GameAttributes(gameID: \"123\", teamAName: \"Hawks\", teamBName: \"Eagles\")\nlet state = GameAttributes.ContentState(teamAScore: 14, teamBScore: 10, quarter: 2, timeRemaining: \"5:23\", lastPlay: nil)\n\nlet encoder = JSONEncoder()\nif let attributesData = try? encoder.encode(attributes),\n   let stateData = try? encoder.encode(state) {\n    let totalSize = attributesData.count + stateData.count\n    print(\"Total size: \\(totalSize) bytes\")\n\n    if totalSize \u003C 2048 {\n        print(\"✅ Safe with room to grow\")\n    } else if totalSize \u003C 3072 {\n        print(\"⚠️ Acceptable but monitor\")\n    } else if totalSize \u003C 3584 {\n        print(\"? Risky - optimize now\")\n    } else {\n        print(\"❌ CRITICAL - will likely fail\")\n    }\n}\n```\n\n**Optimization priorities** (when over 2KB):\n1. Replace `String` descriptions with enums (if fixed set)\n2. Shorten string values (\"Team A\" → \"A\")\n3. Use smaller types (Int → Int8 if range allows)\n4. Remove optional fields that are rarely used\n\n---\n\n## Pattern 7: Widget Not Appearing in Gallery\n\n**Time cost**: 30 minutes debugging invisible widget\n\n### Symptom\n- Widget builds successfully\n- No errors in console\n- Widget doesn't appear in widget picker\u002Fgallery\n- Can't add to Home Screen\n\n### ❌ BAD Code\n\n```swift\n@main\nstruct MyWidget: Widget {\n    var body: some WidgetConfiguration {\n        StaticConfiguration(kind: \"MyWidget\", provider: Provider()) { entry in\n            MyWidgetView(entry: entry)\n        }\n        .configurationDisplayName(\"My Widget\")\n        .description(\"Shows data\")\n        \u002F\u002F ❌ MISSING: supportedFamilies() — widget won't appear!\n    }\n}\n```\n\n**Why it fails**: Without supportedFamilies(), system doesn't know which sizes to offer.\n\n### ✅ GOOD Code\n\n```swift\n@main\nstruct MyWidget: Widget {\n    var body: some WidgetConfiguration {\n        StaticConfiguration(kind: \"MyWidget\", provider: Provider()) { entry in\n            MyWidgetView(entry: entry)\n        }\n        .configurationDisplayName(\"My Widget\")\n        .description(\"Shows data\")\n        .supportedFamilies([.systemSmall, .systemMedium]) \u002F\u002F ✅ Required\n    }\n}\n```\n\n**Other common causes**:\n- Widget target's \"Skip Install\" set to YES (should be NO)\n- Widget extension not added to app's \"Embed App Extensions\"\n- Clean build folder needed (`Cmd+Shift+K`)\n\n---\n\n# Decision Tree\n\n```\nWidget\u002FExtension Issue?\n│\n├─ Widget not appearing in gallery?\n│  ├─ Check WidgetBundle registered in @main\n│  ├─ Verify supportedFamilies() includes intended families\n│  └─ Clean build folder, restart Xcode\n│\n├─ Widget not refreshing?\n│  ├─ Timeline policy set to .never?\n│  │  └─ Change to .atEnd or .after(date)\n│  ├─ Budget exhausted? (too frequent reloads)\n│  │  └─ Increase interval between entries (15-60 min)\n│  └─ Manual reload\n│     └─ WidgetCenter.shared.reloadAllTimelines()\n│\n├─ Widget shows empty\u002Fold data?\n│  ├─ App Groups configured in BOTH targets?\n│  │  ├─ No → Add \"App Groups\" entitlement\n│  │  └─ Yes → Verify same group ID\n│  ├─ Using UserDefaults.standard?\n│  │  └─ Change to UserDefaults(suiteName: \"group.com.myapp\")\n│  └─ Shared container path correct?\n│     └─ Print containerURL, verify not nil\n│\n├─ Interactive button not working?\n│  ├─ App Intent perform() returns value?\n│  │  └─ Must return IntentResult\n│  ├─ perform() updates shared data?\n│  │  └─ Update App Group storage\n│  └─ Calls WidgetCenter.reloadTimelines()?\n│     └─ Reload to reflect changes\n│\n├─ Live Activity fails to start?\n│  ├─ Data size > 4KB?\n│  │  └─ Reduce ActivityAttributes + ContentState\n│  ├─ Authorization enabled?\n│  │  └─ Check ActivityAuthorizationInfo().areActivitiesEnabled\n│  └─ pushType correct?\n│     └─ nil for local updates, .token for push\n│\n├─ Control Center control unresponsive?\n│  ├─ Async operation blocking UI?\n│  │  └─ Use ControlValueProvider with async currentValue()\n│  └─ Provide previewValue for instant fallback\n│\n└─ watchOS Live Activity not showing?\n   ├─ supplementalActivityFamilies includes .small?\n   └─ Apple Watch paired and in range?\n```\n\n---\n\n# Mandatory First Steps\n\nBefore debugging any widget or extension issue, complete this checklist:\n\n## Widget Debugging Checklist\n\n- ☐ **App Groups enabled** in BOTH main app AND extension targets\n  ```bash\n  # Verify entitlements\n  codesign -d --entitlements - \u002Fpath\u002Fto\u002FYourApp.app\n  # Should show com.apple.security.application-groups\n  ```\n\n- ☐ **Widget in Widget Gallery** (not just on Home Screen)\n  - Long-press Home Screen → + button → Find your widget\n  - Verify it appears with correct name and description\n\n- ☐ **Console logs** for timeline errors\n  ```bash\n  # Xcode Console\n  # Filter: \"widget\" OR \"timeline\"\n  # Look for: \"Timeline reload failed\", \"Budget exhausted\"\n  ```\n\n- ☐ **Manual reload test**\n  ```swift\n  WidgetCenter.shared.reloadAllTimelines()\n  ```\n  - If this fixes it → problem is timeline policy or refresh budget\n\n- ☐ **Shared container accessible**\n  ```swift\n  let container = FileManager.default.containerURL(\n      forSecurityApplicationGroupIdentifier: \"group.com.myapp\"\n  )\n  print(\"Container: \\(container?.path ?? \"NIL\")\")\n  \u002F\u002F Must print valid path, not \"NIL\"\n  ```\n\n## Live Activity Debugging Checklist\n\n- ☐ **ActivityAttributes \u003C 4KB**\n  ```swift\n  let encoded = try JSONEncoder().encode(attributes)\n  print(\"Size: \\(encoded.count) bytes\") \u002F\u002F Must be \u003C 4096\n  ```\n\n- ☐ **Authorization check**\n  ```swift\n  let authInfo = ActivityAuthorizationInfo()\n  print(\"Enabled: \\(authInfo.areActivitiesEnabled)\")\n  ```\n\n- ☐ **pushType matches server integration**\n  - `nil` → local updates only\n  - `.token` → expects push notifications\n\n- ☐ **Dismissal policy implemented**\n  - Every activity.end() must specify policy\n\n## Control Center Widget Checklist\n\n- ☐ **ControlValueProvider for async data**\n- ☐ **previewValue provides instant fallback**\n- ☐ **App Intent perform() is async**\n- ☐ **No blocking network\u002Fdatabase calls in views**\n\n---\n\n# Pressure Scenarios\n\n## Scenario 1: \"Widget shows wrong data in production\"\n\n### Situation\n- App released to App Store\n- Users report widget displaying incorrect\u002Fstale information\n- Works fine in development\n\n### Pressure Signals\n- ? **App Store reviews** — 1-star reviews mentioning broken widget\n- ⏰ **Time pressure** — Need hotfix ASAP\n- ? **Executive visibility** — Management asking for status updates\n\n### Rationalization Traps (DO NOT)\n\n1. *\"Just force a timeline reload more often\"*\n   - **Why it fails**: Exhausts budget, makes problem worse\n\n2. *\"The widget worked in testing\"*\n   - **Why it fails**: Development vs. production App Groups mismatch\n\n3. *\"Users should just restart their phone\"*\n   - **Why it fails**: Not a fix, damages reputation\n\n### MANDATORY Systematic Fix\n\n#### Step 1: Verify App Groups (30 min)\n\n```swift\n\u002F\u002F Add logging to BOTH app and widget\nlet group = \"group.com.myapp.production\" \u002F\u002F Must match exactly\nlet container = FileManager.default.containerURL(\n    forSecurityApplicationGroupIdentifier: group\n)\n\nprint(\"[\\(Bundle.main.bundleIdentifier ?? \"?\")] Container: \\(container?.path ?? \"NIL\")\")\n\n\u002F\u002F Log EVERY read\u002Fwrite\nlet shared = UserDefaults(suiteName: group)!\nprint(\"Writing key 'lastUpdate' = \\(Date())\")\nshared.set(Date(), forKey: \"lastUpdate\")\n```\n\n**Verify**: Run app, then widget. Both should print SAME container path.\n\n#### Step 2: Check Container Paths\n\n```bash\n# Device logs (Xcode → Window → Devices and Simulators → View Device Logs)\n# Filter: Your app bundle ID\n# Look for: Container path mismatches\n```\n\nCommon issues:\n- App uses `group.com.myapp.dev`\n- Widget uses `group.com.myapp.production`\n- **Fix**: Ensure EXACT same group ID in both .entitlements files\n\n#### Step 3: Add Version Stamp\n\n```swift\n\u002F\u002F Main app — stamp every write\nstruct WidgetData: Codable {\n    var value: String\n    var timestamp: Date\n    var appVersion: String\n}\n\nlet data = WidgetData(\n    value: \"Latest\",\n    timestamp: Date(),\n    appVersion: Bundle.main.appVersion\n)\nshared.set(try JSONEncoder().encode(data), forKey: \"widgetData\")\n\n\u002F\u002F Widget — verify version\nif let data = shared.data(forKey: \"widgetData\"),\n   let decoded = try? JSONDecoder().decode(WidgetData.self, from: data) {\n    print(\"Widget reading data from app version: \\(decoded.appVersion)\")\n}\n```\n\n#### Step 4: Force Reload on App Launch\n\n```swift\n\u002F\u002F AppDelegate \u002F @main App\nfunc applicationDidBecomeActive(_ application: UIApplication) {\n    WidgetCenter.shared.reloadAllTimelines()\n}\n```\n\n### Communication Template\n\n**To stakeholders**:\n```\nStatus: Investigating widget data sync issue\n\nRoot cause: App Groups configuration mismatch between app and widget extension in production build\n\nFix: Updated both targets to use identical group identifier, added logging to prevent recurrence\n\nTimeline: Hotfix submitted to App Store review (24-48h)\n\nWorkaround for users: Force-quit app and relaunch (triggers widget refresh)\n```\n\n### Time Saved\n- **Without systematic fix**: 4-8 hours of trial-and-error, multiple resubmissions\n- **With this process**: 1-2 hours to identify, fix, and verify\n\n---\n\n## Scenario 2: \"Live Activity must update instantly\"\n\n### Situation\n- Sports score app\n- Users expect scores to update within seconds of real game events\n- Current timeline-based approach too slow\n\n### Pressure\n- **Competitive**: \"Other apps update faster\"\n- **Deadline**: Marketing promised \"real-time\" updates\n\n### Rationalization Traps (DO NOT)\n\n1. *\"Just create entries every 5 seconds\"*\n   - **Why it fails**: Not real-time, exhausts battery, doesn't scale\n\n2. *\"Add WebSocket to widget view\"*\n   - **Why it fails**: Extensions can't maintain persistent connections\n\n3. *\"Lower refresh interval to 1 second\"*\n   - **Why it fails**: Timeline system not designed for sub-minute updates\n\n### MANDATORY Solution: Phased Approach\n\n**Critical reality check**: Push notification entitlement approval takes **3-7 days**. Never promise features before approval.\n\n#### Phase 1: Ship with Local Updates (No Approval Required)\n\n**Ship immediately** with app-driven updates:\n\n```swift\n\u002F\u002F Start activity WITHOUT push (no entitlement needed)\nlet activity = try Activity.request(\n    attributes: attributes,\n    content: initialContent,\n    pushType: nil  \u002F\u002F Local updates only\n)\n\n\u002F\u002F In your app when data changes (user opens app, pulls to refresh)\nawait activity.update(ActivityContent(\n    state: updatedState,\n    staleDate: nil\n))\n```\n\n**Set expectations**: Updates occur when user interacts with app. This is **acceptable** for v1.0 and requires zero approval.\n\n#### Phase 2: Add Push After Approval (3-7 Days)\n\n**After entitlement approved**, switch to push:\n\n#### Step 1: Enable Push for Live Activities\n\n```swift\n\u002F\u002F 1. Entitlement: \"com.apple.developer.activity-push-notification\"\n\n\u002F\u002F 2. Request activity with push token\nlet activity = try Activity.request(\n    attributes: attributes,\n    content: initialContent,\n    pushType: .token\n)\n\n\u002F\u002F 3. Monitor for token\nTask {\n    for await pushToken in activity.pushTokenUpdates {\n        let tokenString = pushToken.map { String(format: \"%02x\", $0) }.joined()\n        await sendTokenToServer(activityID: activity.id, token: tokenString)\n    }\n}\n```\n\n#### Step 2: Server-Side Push (Phase 2 Only)\n\n```json\n{\n  \"aps\": {\n    \"timestamp\": 1633046400,\n    \"event\": \"update\",\n    \"content-state\": {\n      \"teamAScore\": 14,\n      \"teamBScore\": 10,\n      \"quarter\": 2,\n      \"timeRemaining\": \"5:23\"\n    },\n    \"alert\": {\n      \"title\": \"Touchdown!\",\n      \"body\": \"Team A scores\"\n    }\n  }\n}\n```\n\n**Standard push limit**: ~10-12 per hour\n\n#### Step 3: Request Frequent Updates Entitlement (Phase 2, iOS 18.2+)\n\nFor apps requiring more frequent pushes (sports, stocks):\n\n```xml\n\u003Ckey>com.apple.developer.activity-push-notification-frequent-updates\u003C\u002Fkey>\n\u003Ctrue\u002F>\n```\n\n**Requires justification** in App Store Connect: \"Live sports scores require immediate updates for user engagement\"\n\n#### Verification\n\n```swift\n\u002F\u002F Log push receipt in Live Activity widget\n#if DEBUG\nlet logURL = FileManager.default.containerURL(\n    forSecurityApplicationGroupIdentifier: \"group.com.myapp\"\n)!.appendingPathComponent(\"push_log.txt\")\n\nlet timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)\ntry! \"\\(timestamp): Received push\\n\".append(to: logURL)\n#endif\n```\n\n### Communication Template\n\n**To marketing\u002Fexec (Phase 1)**:\n```\nLaunch Timeline:\n- Phase 1 (immediate): Live Activities with app-driven updates. Updates appear when users open app or pull to refresh.\n- Phase 2 (3-7 days): Push notification integration after Apple approval. Updates arrive within 1-3 seconds of server events.\n\nRecommendation: Launch Phase 1 to market, communicate Phase 2 as \"coming soon\" once approved.\n```\n\n**To marketing\u002Fexec (Phase 2)**:\n```\n\"Real-time\" positioning requires clarification:\n\nTechnical: Live Activities update via push notifications with 1-3 second latency from server to device\n\nConstraints: Apple's push system has rate limits (~10\u002Fhour standard, axiom-higher with special entitlement)\n\nCompetitive analysis: Competitors likely use same system with similar limitations\n\nRecommendation: Position as \"near real-time\" (accurate) vs \"instant\" (misleading)\n```\n\n### Reality Check\n- Push notifications are fastest mechanism available\n- 1-3 second latency is normal\n- Budget limits exist for battery optimization\n- Users prefer longer battery life over millisecond-faster scores\n\n> For comprehensive push notification setup (APNs auth, payload format, token management, service extensions), see axiom-push-notifications and axiom-push-notifications-ref. This skill covers the ActivityKit UI and state management side.\n\n---\n\n## Scenario 3: \"Control Center control is slow\"\n\n### Situation\n- Smart home control for lights\n- Tapping control in Control Center takes 3-5 seconds to respond\n- Users expect instant feedback\n\n### MANDATORY Fix: Optimistic UI + Async Value Provider\n\n#### Problem Code\n\n```swift\nstruct LightControl: ControlWidget {\n    var body: some ControlWidgetConfiguration {\n        StaticControlConfiguration(kind: \"Light\") {\n            ControlWidgetToggle(\n                isOn: LightManager.shared.isOn, \u002F\u002F ❌ Blocking fetch\n                action: ToggleLightIntent()\n            ) { isOn in\n                Label(isOn ? \"On\" : \"Off\", systemImage: \"lightbulb.fill\")\n            }\n        }\n    }\n}\n```\n\n#### Fixed Code\n\n```swift\n\u002F\u002F 1. Value Provider for async state\nstruct LightProvider: ControlValueProvider {\n    func currentValue() async throws -> LightValue {\n        \u002F\u002F Async fetch from HomeKit\u002Fserver\n        let isOn = try await HomeManager.shared.fetchLightState()\n        return LightValue(isOn: isOn)\n    }\n\n    var previewValue: LightValue {\n        \u002F\u002F Instant fallback from cache\n        let shared = UserDefaults(suiteName: \"group.com.myapp\")!\n        return LightValue(isOn: shared.bool(forKey: \"lastKnownLightState\"))\n    }\n}\n\nstruct LightValue: ControlValueProviderValue {\n    var isOn: Bool\n}\n\n\u002F\u002F 2. Optimistic Intent\nstruct ToggleLightIntent: AppIntent {\n    static var title: LocalizedStringResource = \"Toggle Light\"\n\n    func perform() async throws -> some IntentResult {\n        \u002F\u002F Immediately update cache (optimistic)\n        let shared = UserDefaults(suiteName: \"group.com.myapp\")!\n        let currentState = shared.bool(forKey: \"lastKnownLightState\")\n        let newState = !currentState\n        shared.set(newState, forKey: \"lastKnownLightState\")\n\n        \u002F\u002F Then update actual device (async)\n        try await HomeManager.shared.setLight(isOn: newState)\n\n        return .result()\n    }\n}\n\n\u002F\u002F 3. Control with provider\nstruct LightControl: ControlWidget {\n    var body: some ControlWidgetConfiguration {\n        StaticControlConfiguration(kind: \"Light\", provider: LightProvider()) { value in\n            ControlWidgetToggle(\n                isOn: value.isOn,\n                action: ToggleLightIntent()\n            ) { isOn in\n                Label(isOn ? \"On\" : \"Off\", systemImage: \"lightbulb.fill\")\n                    .tint(isOn ? .yellow : .gray)\n            }\n        }\n    }\n}\n```\n\n**Result**: Control responds instantly with cached state, actual device updates in background.\n\n---\n\n# Final Checklist\n\nBefore shipping widgets or Live Activities:\n\n## Pre-Release\n- ☐ App Groups entitlement in BOTH targets (app + extension)\n- ☐ Shared UserDefaults uses `suiteName` (not `.standard`)\n- ☐ Timeline entries ≥ 5 minutes apart (avoid budget exhaustion)\n- ☐ No network calls in widget views (only in TimelineProvider)\n- ☐ ActivityAttributes + ContentState \u003C 4KB\n- ☐ Live Activities call `.end()` with appropriate dismissal policy\n- ☐ Control Center controls use ControlValueProvider for async data\n- ☐ Tested on actual device (not just simulator) — **Required because**:\n  - Simulator doesn't enforce timeline budget limits\n  - Push notifications don't work in simulator\n  - App Groups container paths differ (simulator vs device)\n  - Memory limits not enforced in simulator\n  - Background refresh behavior different\n- ☐ Tested all supported widget families\n- ☐ Verified widget appears in Widget Gallery\n\n## Post-Release Monitoring\n- ☐ Monitor for \"Timeline reload budget exhausted\" errors\n- ☐ Track widget data staleness in analytics\n- ☐ Watch App Store reviews for widget-related complaints\n- ☐ Log App Group container access for debugging\n\n## Common Failure Modes\n- Missing App Groups → Widget shows default data\n- Wrong group ID → App and widget can't communicate\n- Over-refreshing → Widget stops updating after hours\n- Network in view → Widget renders blank\n- No dismissal policy → Zombie Live Activities\n- Blocking main thread → Unresponsive controls\n\n---\n\n**Remember**: Widgets are NOT mini apps. They're glanceable snapshots rendered by the system. Extensions run in sandboxed environments with strict resource limits. Follow the patterns in this skill to avoid the most common pitfalls.\n\n---\n\n## Resources\n\n**Skills**: axiom-extensions-widgets-ref, axiom-push-notifications, axiom-push-notifications-ref, axiom-background-processing\n\n",{"name":5,"license":15,"metadata":43,"description":7,"compatibility":45},{"version":44},"1.0.0","iOS 14+, iPadOS 14+, watchOS 9+","main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002FCharlesWiltgen\u002FAxiom\u002Faxiom-extensions-widgets",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":56,"prompt":66,"zipAvailable":67},[57,60,63],{"tool":58,"command":59},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill axiom-extensions-widgets",{"tool":61,"command":62},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill axiom-extensions-widgets",{"tool":64,"command":65},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill axiom-extensions-widgets","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002FCharlesWiltgen\u002FAxiom\u002Faxiom-extensions-widgets\u002FSKILL.md",true,[],[70,84,94,103,109,116],{"skillNo":71,"name":72,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":73,"categories":74,"keywords":80,"publishedAt":38},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[75],{"categoryNo":76,"categoryName":77,"taxonomyType":78,"taxonomyTypeDesc":79},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[81,82,83],"model","model hierarchy","model hierarchy skill",{"skillNo":85,"name":86,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":90,"publishedAt":38},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[89],{"categoryNo":76,"categoryName":77,"taxonomyType":78,"taxonomyTypeDesc":79},[91,92,93],"MCP","zoom","zoom mcp",{"skillNo":95,"name":96,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":97,"categories":98,"keywords":99,"publishedAt":38},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[100,101,102,92],"sdk","zoom apps sdk","apps",{"skillNo":104,"name":105,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":106,"categories":107,"keywords":108,"publishedAt":38},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[105],{"skillNo":110,"name":111,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":112,"categories":113,"keywords":115,"publishedAt":38},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[114],{"categoryNo":76,"categoryName":77,"taxonomyType":78,"taxonomyTypeDesc":79},[111],{"skillNo":117,"name":118,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":119,"categories":120,"keywords":121,"publishedAt":38},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[118]]