Skip to content

Add SwiftUI observation mount tests#112

Draft
woodymelling wants to merge 2 commits into
skiptools:mainfrom
woodymelling:observation
Draft

Add SwiftUI observation mount tests#112
woodymelling wants to merge 2 commits into
skiptools:mainfrom
woodymelling:observation

Conversation

@woodymelling
Copy link
Copy Markdown
Contributor

@woodymelling woodymelling commented May 20, 2026

This is the first small slice towards achieving true Observation support for skip-ui

The issue isn't that it's unavailable, but there's a gap in the Skip bridge path to trigger recomposition.

That works for app-level @Observable models that can import SkipFuse, but it does not cover third-party libraries that manually implement Observable with ObservationRegistrar / mutation notifications and do not want to depend on Skip

I'll try to make the commits structured in a way where we can discuss it at the different steps.

This PR does not try to solve the problem yet. It only adds a minimal mounted-runtime test:

  1. Mount UI that reads an observable value.
  2. Mutate that value.
  3. Assert the already-mounted Compose UI updates.

This first commit just exposes the problem to testing.
If I create an object that conforms to Observable, but doesn't apply the macro, it does not observe the changes on the type.

The test sets up a harness to check rendering changes. The important part is that this is not a fresh Swift read after mutation. It checks whether Skip/Compose receives the Observation invalidation and recomposes.

Current behavior:

skip test builds the project, mounts the test view, and confirms the initial render. The failure happens only after mutating the observed value:

[✓] Build Project (9.51s)
[✗] Test project (135.01s)
...
java.lang.AssertionError: Assert failed:
The component with Text + InputText + EditableText contains 'count: 1'
(ignoreCase: false) is not displayed!
...
Failed tests:
skip.swift.ui.ObservationMountRuntimeTests.testMountedViewInvalidatesWhenObservablePropertyChanges

These do pass with the similar SwiftUI only harness.

  • REQUIRED: I have signed the Contributor Agreement
  • REQUIRED: I have tested my change locally with swift test
  • OPTIONAL: I have tested my change on an iOS simulator or device
  • OPTIONAL: I have tested my change on an Android emulator or device

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.

I used an LLM to help me get the harness into place. Runtime code will be written by hand


Relevant context:

@cla-bot cla-bot Bot added the cla-signed label May 20, 2026
@woodymelling woodymelling marked this pull request as draft May 20, 2026 01:47
@woodymelling
Copy link
Copy Markdown
Contributor Author

woodymelling commented May 20, 2026

I just upgraded the test to validate that Observations 'withObservationTracking' call will properly observe the state changes.

@woodymelling
Copy link
Copy Markdown
Contributor Author

I've added the companion PR's for this. Lets try to keep discussion centered here for the most part.

Getting this compiling with the skipstone changes was a bit of a challenge, but I think it's working when I point at local repos.

skiptools/skipstone#251
skiptools/skip-android-bridge#25

This makes each View insert a withObservationTracking to drive invalidation.

We need to be careful to only read the View body in the withObservationTracking closure. We can then convert it to the java Object.


I have a small runtime exploration here that I've been using.

https://tangled.org/woody.fm/skip-observation-exploration/blob/main/Sources/TCA26SkipExplorations/ContentView.swift

At this point, the views are re-rendering, but I'm seeing that the Views are still getting invalidated to often.

In the second example, both views are re-rendering, but in SwiftUI, only one renders at a time.

@woodymelling
Copy link
Copy Markdown
Contributor Author

skiptools/skipstone@bbff959

So this commit opts the top level skip.ui View into Renderable. This keeps it from flattening it's children when the parent calls Evaluate(). This matches the SwiftUI behavior where a var body: some View becomes a recomputation boundary.

This should prevent Skip from over rendering, but it may have some larger impacts.

On a positive note, this seems to work!

Screen.Recording.2026-05-21.at.3.28.03.PM.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant