Add static attributes and unit tests for Live Activity types#2
Open
Add static attributes and unit tests for Live Activity types#2
Conversation
Attributes are set at creation time and immutable for the activity's lifetime — only ContentState changes via updates. Both types now have a name: String property displayed in the widget Lock Screen view. Push-to-start payloads include attributes; update payloads do not. <log> ### Human prompts (VERBATIM): 1. "The comments need changing. Extract the SDK specific parts to an "sdk proposal" doc. Keep the comments in the code focused on what the code is doing and how the liveactivities lifecycle works. Think "LiveActivities tutorial project", with an sdk proposal to the side" → Claude: Created SDK_PROPOSAL.md, rewrote all source comments to tutorial-focused. Force-pushed to update PR. 2. "Question -- push to start is one token, update pushes are always a different token?" → Claude: Explained the two token types — push-to-start (per-type, per-device, persistent) vs activity push tokens (per-instance, ephemeral). 3. "When you say "gets back an activity push token" -- how?" → Claude: Corrected imprecise language. The server doesn't get anything back from APNs. The app discovers new activities via Activity<T>.activityUpdates, observes pushTokenUpdates, and sends the token to the server via API call. Flagged activityUpdates observation as a gap in the reference implementation. 4. "So what we need is - At app launch, register and capture all push-to-start tokens - Also at app launch (?) register for pushTokenUpdates - Submit push-to-start and and push token updates to the server In the push-to-start case -- does the main app wake up? How are we actually going from "push to start" to "now we have update tokens"? What state is the app in?" → Claude: Explained push-to-start does NOT wake the main app. System creates activity from push payload alone. There's a gap where the server can't send updates until the app launches and discovers the activity. Outlined the full launch registration pattern. 5. "No, no. I think we're getting to a point where we just need to play with things and see how they actually work. How are you feeling about that?" → Claude: Agreed — too much theory, need to run it. 6. "What would a minimal "push to start" push payload look like for either live activity?" → Claude: Provided minimal JSON payload and APNs headers. Noted uncertainty about Date encoding strategy (deferredToDate vs iso8601). 7. "Two things -- since attributes are apparently quite different, we should extend the sample to support them. To be clear, those are fixed from start to dismiss and cannot be updated?" → Claude: Confirmed attributes are immutable for the activity's lifetime. Proposed adding name: String to both types. 8. "And the other thing was "what of this can we unit test"" → Claude: Identified testable surface — Codable round-trips, JSON shape validation, schema introspection. Cannot test Activity.request, push tokens, widget rendering. 9. "Let's merge the current PR, then add attributes, then add unit tests. Use logical commits" → Claude: Merged PR #1, created new branch, added name attribute to both types, updated widgets and ContentView. </log> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
9 tests covering both activity types: - Attributes and ContentState encode/decode round-trips - JSON shape validation (correct keys, correct types, correct count) - Cross-type schema distinction (status vs phase) - Default date encoding behavior (deferredToDate produces a number) These validate the contract with APNs — if the JSON shape is wrong, push-based updates silently fail. See first commit in this branch for full human interaction log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ayload shape. Clarifies that attributes are immutable for the activity's lifetime and only included in push-to-start payloads, not update payloads. Updates the start payload example to include the name attribute. See first commit in this branch for full human interaction log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Push-to-start tokens are per-type, not per-instance. Observing them in the start/dismiss cycle caused accumulating observers — each new Task received the current token immediately, and cooperative cancellation didn't fully clean up old observers. Now observed once on view appear with a guard to prevent re-registration. <log> ### Human prompts (VERBATIM): 1. "I seem to get an extra push-to-start token logged each time I start/dismiss a live activity" → Claude: Identified the bug — creating a new pushToStartTokenUpdates observer per start/dismiss cycle. Each new observer gets the current token immediately and old observers accumulate due to cooperative cancellation. Moved observation to onAppear, observe once per type. </log> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Push-to-start tokens, activityUpdates, and existing activity discovery now run in didFinishLaunchingWithOptions instead of SwiftUI onAppear. onAppear was inconsistent — it depends on view lifecycle and scene restoration. AppDelegate fires reliably on every launch. Also observes Activity<T>.activityUpdates to discover activities created by push-to-start, and checks Activity<T>.activities at launch for any that existed while the app was killed. <log> ### Human prompts (VERBATIM) since last commit: 1. "I seem to get an extra push-to-start token logged each time I start/dismiss a live activity" → Claude: Identified accumulating observers. Moved PTS observation to onAppear with guard. 2. "Can we add some kind of logging to try and determine if the app is starting due to a tap on a live activity?" → Claude: Added launch logging (launchOptions, applicationState, configurationForConnecting). 3. "I don't seem to be getting anything in the logs." → Claude: SwiftUI owns the scene lifecycle with @UIApplicationDelegateAdaptor. Added onOpenURL, onContinueUserActivity(BrowsingWeb), scenePhase logging. 4. "I'm getting the scene phase logging, but nothing else" → Claude: Tapping a Live Activity with no widgetURL just opens the app with no special context. 5. "Well, no, one second. I'm not even _getting_ the "didFinishLaunchingWithOptions" callback when coming back from background from a live activity tap. Are there other lifecycle hooks we should be looking at, or does that seem fishy to you?" → Claude: didFinishLaunchingWithOptions only fires on cold launch, not resume from background. 6. "... no this doesn't feel right to me. We can get information on if a push notification is tapped to foreground the app. I expect live activities to have something similar, not need weird workarounds" → Claude: Researched — found NSUserActivityTypeLiveActivity constant from WidgetKit. Previous onContinueUserActivity was listening for wrong type (BrowsingWeb). 7. "That worked. Also we are long overdue to commit work. Can you chunk this out into logical commits? Are there git commands you could use to add specific hunks from individual files?" → Claude: Stashed and rebuilding commits in logical order. 8. "I don't seem to consistently get the push to start tokens logged at app launch. Thoughts?" → Claude: onAppear unreliable. Moved observation to AppDelegate.didFinishLaunchingWithOptions. 9. "Yes, thank you" → Claude: Moved all observation to AppDelegate, cleaned up ContentView. </log> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without this flag, push-to-start delivery was inconsistent — sometimes working, sometimes silently dropped. With the flag, delivery is reliable. Grants a higher push update budget. Users can disable in Settings (which ends all ongoing Live Activities). See previous commit for full human interaction log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Timer now has only status: String. Countdown keeps endDate: Date + phase. Having one type with a Date field and one without lets us compare push payload behavior — Date encoding (deferredToDate) was a source of confusion during push-to-start testing. See previous commits for full human interaction log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Handle NSUserActivityTypeLiveActivity via onContinueUserActivity to detect when the app is opened by tapping a Live Activity - Log scenePhase transitions for debugging foreground/background behavior - Log onOpenURL for widgetURL-based deep links - Log didFinishLaunchingWithOptions keys and applicationState at launch - Add configurationForConnecting logging (may not fire with SwiftUI @UIApplicationDelegateAdaptor — SwiftUI owns scene configuration) Discovered: without widgetURL, tapping a Live Activity delivers NSUserActivityTypeLiveActivity via the user activity continuation path. This is analogous to push notification tap handling but uses WidgetKit's activity continuation mechanism instead of UNUserNotificationCenter. See previous commits for full human interaction log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document findings from live device testing: - alert key required for start events (silently dropped without it) - NSSupportsLiveActivitiesFrequentUpdates required for reliable delivery - content-state must include ALL fields (incomplete shows loading spinner) - Date encoding uses deferredToDate (timeIntervalSinceReferenceDate) - Update payload examples to use correct date encoding format See previous commits for full human interaction log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
name: Stringstatic attribute to bothTimerActivityAttributesandCountdownActivityAttributes— set at creation, immutable for the activity's lifetime, displayed in widget Lock Screen viewsTeakSwiftCleanroomPodsTests) with 9 tests covering Codable round-trips, JSON shape validation, cross-type schema distinction, and default date encoding behaviorSDK_PROPOSAL.mdto document static vs dynamic attribute distinction and push-to-start payload shapeTest plan
xcodebuild testpasses all 9 testsSDK_PROPOSAL.mdstatic attributes section🤖 Generated with Claude Code