Repository navigation
Conversation
A private or fileprivate @State/@FocusState/@GestureState/@AppStorage/ @Environment property has no name the generated bridge file can reach, so it errored with kotlinBridgeStatePrivate. SkipSwiftUI can now find a view's dynamic properties at runtime instead of by name (companion skip-fuse-ui change), so this switches to that path per view, only when it has such a property. addSwiftUIImplementation now emits one of two Evaluate bodies: unaffected views keep today's per-name rememberSaveable/environment calls, byte-for-byte, so every existing BridgeToKotlinTests expectation is untouched; a view with any private/fileprivate property switches its whole Evaluate to a generic loop over Swift_dynamicPropertyKinds, syncing each property by index through six new fixed-name external functions instead of per-name ones. A generic view's type-erased peer is given no per-name closures in that case, since nothing needs to close over a name it cannot see. Adds five tests: a private-state view, a fileprivate-environment view, one mixing an internal and a private property (both end up bridged dynamically), a generic private-state view (confirming its type-erased peer gets no per-name closures), and a private-state ViewModifier. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
swiftUIEvaluate and swiftUIEvaluateDynamic each built the same override declaration: parameters, return type, @composable, and the trailing super.Evaluate call. Both now pass their body lines to one helper. Generated output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The existing tests switch views to the dynamic path through @State and @Environment only. This one uses the other three recognised attributes, two private and one fileprivate, and expects the same dynamic Evaluate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generated Kotlin switches on codes that SkipSwiftUI's Java_dynamicPropertyKinds produces; say so next to them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A private property implies a non-empty list, so the dynamic branch can come first and the per-name one follow as `else if`, leaving its lines as they were. The generic peer target is spelled through peerSwiftTarget, like the other genericvalue reads. Generated output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Open
5 tasks done
Ceylo
marked this pull request as ready for review
September 27, 2026 22:01
Ceylo
marked this pull request as draft
September 28, 2026 00:30
5 tasks done
Ceylo
marked this pull request as ready for review
September 28, 2026 19:30
Merged
2 of 5 tasks
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.
Paired with skip-fuse-ui: skiptools/skip-fuse-ui#151 (branch
Ceylo/skip-fuse-ui:feature/private-state-bridging). That one merges first; this one needs its runtime entry points. A follow-up there then uncomments its private-state fixture and test, which need a skipstone release carrying this PR (its diff).What
With this pair, a Fuse view can keep its
@State,@FocusState,@GestureState,@AppStorageand@Environmentpropertiesprivate, as SwiftUI code normally does, instead of widening them to internal just so the bridge can reach them. Today a Fuse view with aprivateorfileprivate@State,@FocusState,@GestureState,@AppStorageor@Environmentproperty fails to transpile withkotlinBridgeStatePrivate("Private state property 'x' cannot be bridged to Android. Consider making this property internal"), because the generated bridge file syncs each property by name and cannot see a private one. With this PR such a view builds: itsEvaluateasks SkipSwiftUI for the view's dynamic properties, found at runtime by field reflection (the paired skip-fuse-ui PR), and syncs each by index instead of by name.Before / after
@State private var count = 0→error: Private state property 'count' cannot be bridged to AndroidEvaluateloops overSwift_dynamicPropertyKinds(Swift_peer)and syncs each property throughSwift_initDynamicState/Swift_syncDynamicState(or the AppStorage/Environment equivalents) by indexThe choice is made per view, in
addSwiftUIImplementation:BridgeToKotlinTestsexpectations are untouched and pass.private/fileprivateone: the whole view'sEvaluateswitches to the index-based loop. It gets seven fixed-name external functions: the kinds query, plus six index-taking init/sync functions forwarding to SkipSwiftUI'sJava_*Dynamic*entry points. A generic view's type-erased peer is then given no per-name closures, since nothing refers to them.kotlinBridgeStatePrivateis removed, since nothing emits it any more.Side effects
Evaluatescaffolding was factored into one helper, and the unchanged expectations show it emits the same text.privatechanges the view's generated code. ItsrememberSaveablecalls move insidekey(i)in a loop, so saved state keyed under the old per-name layout is not carried over across that one source change.Java_dynamicPropertyKindsand six sibling functions. Those exist only in skip-fuse-ui with the paired PR, so this skipstone must ship with (or after) it. A project with a newer skipstone and an older skip-fuse-ui gets a "cannot find 'Java_dynamicPropertyKinds'" build error in generated code, where it used to get thekotlinBridgeStatePrivatemessage. That can only happen to a view that previously failed to build anyway.Known limits
private structview orViewModifiergets no bridge at all. This covers private properties of a type the bridge can name.DynamicPropertystructs. AStatenested inside a user-definedDynamicPropertyis not discovered. That's a candidate for a follow-up.@State,@FocusState,@GestureState,@AppStorage,@Environment). A view switches to runtime discovery only when one of those attributes, written literally, marks aprivate/fileprivateproperty. Once a view is on that path, every conforming wrapper in it is found by type.Verified
swift buildsucceeds.swift test --parallel: 949 tests, 948 pass. The one failure,SkipRunnerTests/testSnippets, compares"1 KB"against a French-locale"1 ko"fromByteCountFormatter. It fails the same way on unmodifiedmainon this machine and is unrelated.BridgeToKotlinTests: 148 pass, the 142 existing ones with unchanged expectations plus 6 new ones:testPrivateStatetestFileprivateEnvironmenttestMixedInternalAndPrivateState(both properties end up on the dynamic path)testGenericPrivateState(the type-erased peer gets no per-name closures)testPrivateStateViewModifiertestPrivateAppStorageFocusStateAndGestureStateEach expectation was filled from the transpiler's actual output, then read by hand.
Mutation. With the private-property check forced off,
testPrivateAppStorageFocusStateAndGestureStatefails, so the new tests do exercise the switch.Cross-repo, host bridge. skip-fuse-ui's JVM-hosted
FuseComposeUITests.testPrivateCounterIncrements(committed commented out in Brdige NSNumber and fix some newly-discovered bridging bugs #151 until this ships, uncommented for this run) exercised this generator's output end to end, built with this branch asSKIPLOCAL: it passed, and it failed on its own assertion when theStateconformance was removed. Caveat: since skip-fuse-ui Make all @_cdecl functions public in order to prevent removal in release builds #134, those Compose tests skip on a macOS host unless run against a device. That run re-enabled the host main-actor hook locally, and the change is not part of either PR.Android emulator, in a real app. Also built into the Android port of FurAffinityApp (Skip Fuse; this patch on a local branch) and run on an API 37 emulator: 32 views with
privatestate or environment properties took the new path. Their state interactions (toggles persisting across a restart, text-field focus, sheets, a deep-link highlight) behaved as before, and frame times over a scripted scroll stayed within stock skipstone's run-to-run spread, with the dynamic path at ≤ 0.035 % CPU.FuseComposeUITestson a device: blocked. An attempt against the same API 37 (Android 17) emulator (ANDROID_SERIAL) of the paired skip-fuse-ui PR's tests, built with this branch asSKIPLOCAL, could not run anyFuseComposeUITestscase, the existingtestCounterIncrementsincluded: each fails before composing with Espresso'sNoSuchMethodException: android.hardware.input.InputManager.getInstance, a method API 37 no longer has. This CI's Fuseskip init … --bridgedemulator job exercises the bridged output on a device.Skip Pull Request Checklist:
swift testWritten with Claude Code: the patch and its tests. Every claim above comes from a run:
swift build,swift test --parallel(withBridgeToKotlinTestsexpectations read against the transpiler's output), the mutation run above, the paired skip-fuse-ui PR's Robolectric tests built with this branch asSKIPLOCAL, and the FurAffinityApp build and emulator runs.