Add NavigationStack, TabView, sheet, refreshable, lazy stacks and Observation support - #4
Merged
Merged
Conversation
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.
Adds the SwiftUI surface a real app needs:
NavigationStack,TabView,.sheet,.refreshable,lazy stacks, and Swift Observation support.
Motivation: migrating BluetoothExplorer
off Skip onto AndroidSwiftUI. Its UI compiled against the system SwiftUI but hit six gaps here; this
closes all of them, so the same view code now builds for both platforms behind
#if canImport(SwiftUI).What's added
NavigationStack— reuses the existingNavigationContextrather than duplicating navigation.NavigationLinkpush and hardware-back pop are shared withNavigationView: on the Android sideAndroidNavigationContainerwas de-genericised so both containers drive the sameBackHandlerView.TabViewwith.tabItemand selection. Tab item labels are written through the existing_ViewTraitKeymachinery (the same mechanism.tag()uses). Because the Android side works fromParentView.childrenand never sees the mounted trait store,_TabViewProxywalks each tab'smodifier chain and reads the values straight off the
_TraitWritingModifiers — which required asmall internal
_AnyModifiedContentconformance onModifiedContentto erase the generics. Only theselected tab is mounted; the content and tab-bar containers are deliberately non-generic so
switching updates them in place rather than remounting (
AndroidRenderer.mountTargetalwaysappends, so a remount would push content below the bar).
.sheet(isPresented:onDismiss:content:)plus theitem:overload — implemented as afull-screen overlay in the same view hierarchy, not an Android
Dialog. The fiber renderer mountschildren by
addViewinto the parent'sViewGroupand has no path to aDialog's separate decorview.
BackHandlerViewis already aFrameLayout, so it doubles as the container and givesback-button dismissal for free.
.refreshable— stores aRefreshActionin the environment exactly as SwiftUI does, and acceptsasyncclosures. No gesture triggers it yet: bindingSwipeRefreshLayoutwould need an androidxdependency this package doesn't have. Code reading
@Environment(\.refresh)works today.LazyVStack/LazyHStack— eager, mapping onto the same LinearLayout path asVStack/HStack.Laziness is an optimisation, not a semantic requirement; noted in the source.
Swift Observation —
.environment(object)and@Environment(Type.self)forAnyObject & Observable. Invalidation wraps the three render entry points inwithObservationTracking, so any property read while producing a body is registered;onChangefunnels through
schedulerinto the samequeueUpdatepathobjectWillChangealready uses, whichkeeps reconciler mutation on the main thread and guarantees the re-render sees the new value.
Re-arming is automatic — the re-render calls
renderagain. Observable objects live in a dictionaryseparate from
EnvironmentValues.values, so they can never collide withEnvironmentKeyvalues.ObservableObject/@EnvironmentObject/@StateObjectare untouched and both systems coexist — thiscore relies on the old one itself, e.g.
NavigationContext.Verification
Each piece was developed in isolation and built with
swift build --swift-sdk swift-6.3.3-RELEASE_android; the combined branch was then built again toconfirm they compose —
Build complete!, no new warnings. The Observation change wasadditionally checked against a consumer-shaped file mixing
@Observable,@Environment(Type.self),@Environment(\.keyPath),@EnvironmentObjectand@State, to prove no overload ambiguity wasintroduced; that file was removed before committing.
None of this has run on a device or emulator. It is verified by construction and by compilation
only, which is the main risk in this PR — particularly for Observation, where the wiring is
analogous to the working
objectWillChangepath but unproven empirically.Known limitations
Each is documented in the source rather than left to be discovered:
@Bindable;@Environment(Store.self) var store: Store?(optional form)unsupported — a missing injection traps rather than yielding
nil. Properties must be read duringbody evaluation to be tracked (same as SwiftUI).
@Stateresets, where real SwiftUI keeps tabidentity alive. No
TabViewStyle, paging, page indicator or.badge.@Environment(\.dismiss)— contentdismisses itself through the binding. Visually closer to
fullScreenCoverthan an iOS card. If appcode sets
isPresented = falsedirectly the sheet unmounts butonDismissdoes not fire.NavigationStack(path:)and value-basednavigationDestination(for:)are notimplemented —
NavigationContext.pathholds type-erased views, not aHashabledata path.spacingandpinnedViewsare accepted for source compatibility and ignored,matching how the existing
VStack/HStackAndroid rendering already drops spacing.Note on an existing bug (not touched)
VStack/HStackboth use_alignment.vertical.gravity, which makesVStack(alignment: .leading)centre its children. The new lazy stacks map
alignmentto the correct cross axis instead, so theyand their eager counterparts currently disagree. Left alone deliberately — worth a separate fix.