Repository navigation
Conversation
refresh-indicator names an indicator a plugin registers on the native side and refresh-indicator-options hands it its settings (camelCase spellings and NativeList::refreshIndicator() too). They serialize as the refresh_indicator and refresh_indicator_options props; the options travel as a JSON object since props carry no maps. Without a name, no prop is sent and the list keeps the platform spinner. 🤖 Généré avec Claude Code
NativeUIRefreshIndicators.register(name) { scope -> … } registers an
indicator, typically from a plugin's android.init_function. A list whose
refresh_indicator names it draws it on the stock indicator's disc, through
PullToRefreshDefaults.IndicatorBox, so it slides with the pull, is clipped
to the list's top edge and casts the stock shadow; the indicator only
fills the 48dp disc, inset by 6dp. Its scope gives the pull's progress
(0..1, read lazily), whether a refresh is running and the decoded
refresh_indicator_options. A list naming no indicator, or one nobody
registered, keeps the stock spinner. mobile-ui itself takes on no new
dependency.
🤖 Généré avec Claude Code
SwiftUI's .refreshable only shows the system spinner, so a list drawing a plugin's indicator needs its own pull-to-refresh. NativeUIPullToRefresh is its plain Swift core: fed the list's top overscroll and whether a finger is on it, it gives the pull's progress up to an 80pt threshold, starts a refresh only when a finger lets go past it and never during another, keeps 60pt of room above the content while refreshing, and places the indicator in the gap the pull opens. tests/ios is a Swift package that unit-tests such logic on macOS (swift test --package-path tests/ios), linking the shipped sources. 🤖 Généré avec Claude Code
NativeUIRefreshIndicators.shared.register(name) { scope in AnyView(…) }
mirrors the Android registry, for a plugin's ios.init_function. The scope
gives the pull's progress (0..1, 1 while refreshing), whether a refresh is
running and the list's refresh_indicator_options decoded from JSON
(missing or malformed options read as none).
The tests/ios package now builds in Swift 5 mode, as the app does.
🤖 Généré avec Claude Code
.refreshable only shows the system spinner, so a list whose refresh_indicator names a registered indicator runs its own pull-to-refresh instead: onScrollGeometryChange feeds the top overscroll to NativeUIPullToRefresh, onScrollPhaseChange tells it when the finger lets go, and the indicator draws in a 40pt square in the gap the pull opens above the rows, clipped to it. A release past the threshold calls on_refresh with a light haptic and keeps room above the rows for a second, as long as the .refreshable path waits, then lets them settle back. A list naming no indicator, or one nobody registered, keeps .refreshable. 🤖 Généré avec Claude Code
Covers the refresh-indicator attributes and fluent method for screens, registering an indicator from a plugin on Android and iOS, the fallback to the platform spinner, and how each platform draws it. 🤖 Généré avec Claude Code
…hing The indicator vanished on release while the list kept the refresh's room above its rows. Its position assumed the scroll geometry's top inset counts the content margin the refresh adds; if it doesn't, the gap was measured a margin too tall and drawn a margin too high, behind the navigation bar. The rows' rest is now the top inset seen outside a refresh, and the gap is how far the rows sit below it, so either reading of the inset gives the same gap and position. 🤖 Généré avec Claude Code
On a List, contentMargins for the scroll content pad inside the content: the scroll offset settles back to its rest while the rows stay shifted, so the indicator's gap closed and it rose out of view during the refresh. The content height also grew by far more than the margin. A top safe area padding insets the scroll view instead: the rows rest on it with the offset reflecting the room, which is what the gap measures. 🤖 Généré avec Claude Code
Nothing told the user how far to pull: a release just short of the threshold drew most of the artwork yet didn't refresh. Crossing the threshold while pulling now plays the gesture-threshold haptic and bounces the indicator inside its disc, once per crossing, so every registered indicator gets it. The threshold is where PullToRefreshBox's distance fraction passes 1, the same point the indicator's progress reaches 1. 🤖 Généré avec Claude Code
As on Android, crossing the threshold while pulling now bounces the indicator's square and plays a medium impact, once per crossing. The light impact played as the refresh started is gone: the buzz now says "you can let go", not "it's refreshing". 🤖 Généré avec Claude Code
🤖 Généré avec Claude Code
🤖 Généré avec Claude Code
This was referenced Nov 3, 2026
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.
What
<native:list on-refresh>always shows the platform spinner. This lets a list name an indicator that a plugin registered, and pass it options:If no installed plugin registered that name, the list keeps the platform spinner, so a screen can name an indicator whether or not the app ships it. mobile-ui ships no indicator itself and adds no dependency.
How
init_function.NativeUIRefreshIndicators.register("name") { scope -> … }NativeUIRefreshIndicators.shared.register("name") { scope in AnyView(…) }0at rest,1exactly where letting go refreshes), whether a refresh is running, and the options decoded from JSON. Props carry no maps, so the options travel as a JSON string.PullToRefreshBox..refreshablecan't show a custom view, so a list with a registered indicator runs its own pull-to-refresh, built ononScrollGeometryChange/onScrollPhaseChange. The indicator fills a 40pt square in the gap the pull opens. While refreshing, the room above the rows is kept withsafeAreaPadding;contentMarginspads inside the content, which made the scroll offset lose track of it. Lists without a registered indicator keep.refreshable.native:listsupports this;native:refreshablekeeps the platform spinner.Example plugin
jubianchi/nativephp-lottie-refresh-indicator uses this registry to draw a Lottie / dotLottie animation that follows the pull, then loops while refreshing. It supports
pull/refreshmarkers and a dark-mode asset. It has been run on devices in a real app, on Android and iOS; its README has recordings.Docs
Companion docs PR: NativePHP/nativephp.com#600. It documents the list attributes and the plugin-side registration, and should merge once this ships.
Tests
refresh-indicator/refresh-indicator-options(tests/ListTest.php). Full suite green.tests/android):tests/ios(swift test --package-path tests/ios). It covers the gesture logic inNativeUIPullToRefreshand the registry. The SwiftUI wiring was checked on simulator and device.tests/android/README.md,tests/ios/README.md).🤖 Generated with Claude Code