Skip to content

Add static attributes and unit tests for Live Activity types#2

Open
AlexSc wants to merge 9 commits intomainfrom
feat/live-activity-attributes-and-tests
Open

Add static attributes and unit tests for Live Activity types#2
AlexSc wants to merge 9 commits intomainfrom
feat/live-activity-attributes-and-tests

Conversation

@AlexSc
Copy link
Copy Markdown
Member

@AlexSc AlexSc commented Mar 31, 2026

Summary

  • Add name: String static attribute to both TimerActivityAttributes and CountdownActivityAttributes — set at creation, immutable for the activity's lifetime, displayed in widget Lock Screen views
  • Add unit test target (TeakSwiftCleanroomPodsTests) with 9 tests covering Codable round-trips, JSON shape validation, cross-type schema distinction, and default date encoding behavior
  • Update SDK_PROPOSAL.md to document static vs dynamic attribute distinction and push-to-start payload shape

Test plan

  • xcodebuild test passes all 9 tests
  • Build succeeds for all 5 targets
  • Run on simulator: activity Lock Screen views show the name attribute
  • Review SDK_PROPOSAL.md static attributes section

🤖 Generated with Claude Code

AlexSc and others added 9 commits March 31, 2026 17:05
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant