Fix sitemap scroll hitching under frequent long-poll updates#1138
Merged
Fix sitemap scroll hitching under frequent long-poll updates#1138
Conversation
The SwiftUI sitemap introduced in 3.2.x hitches during inertial scrolling when items update frequently. Every long-poll result previously called reconcileWidgets + copyWidgetProperties unconditionally, then always reassigned @published rowInputs — causing a broad objectWillChange broadcast on every cycle even when nothing the list renders had changed. Fix: snapshot row inputs from fresh page data before any widget mutation. updateUI now returns early (skipping all reconciliation and @published writes) when the computed preview inputs equal the current rowInputs and the page title is unchanged. When searchText is active, reconciliation still runs to keep currentPage fresh so clearing the filter won't expose stale hidden widgets. Widen widget-version granularity from bare widgetId to full RowID so repeated widget IDs on one page track independently. widgetUpdateVersions is now keyed by RowID.rawValue; bumpWidgetVersions increments by row- identity pairs; SliderRowView, SegmentedRowView, SelectionRowView, and ImageRowView pass input.rowID to the lookup; affected row-input types carry their rowID. Adds SitemapPageViewModelDiffingTests covering: stable rebuild equality, objectWillChange suppression on unchanged data, structure-change rebuild, bumpWidgetVersions no-op on identical inputs, per-row version isolation, structure-change full bump, and repeated-widgetId independence. Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
5 tasks
timbms
added a commit
that referenced
this pull request
Apr 18, 2026
timbms
added a commit
that referenced
this pull request
Apr 18, 2026
…1138) The SwiftUI sitemap introduced in 3.2.x hitches during inertial scrolling when items update frequently. Every long-poll result previously called reconcileWidgets + copyWidgetProperties unconditionally, then always reassigned @published rowInputs — causing a broad objectWillChange broadcast on every cycle even when nothing the list renders had changed. Fix: snapshot row inputs from fresh page data before any widget mutation. updateUI now returns early (skipping all reconciliation and @published writes) when the computed preview inputs equal the current rowInputs and the page title is unchanged. When searchText is active, reconciliation still runs to keep currentPage fresh so clearing the filter won't expose stale hidden widgets. Widen widget-version granularity from bare widgetId to full RowID so repeated widget IDs on one page track independently. widgetUpdateVersions is now keyed by RowID.rawValue; bumpWidgetVersions increments by row- identity pairs; SliderRowView, SegmentedRowView, SelectionRowView, and ImageRowView pass input.rowID to the lookup; affected row-input types carry their rowID. Adds SitemapPageViewModelDiffingTests covering: stable rebuild equality, objectWillChange suppression on unchanged data, structure-change rebuild, bumpWidgetVersions no-op on identical inputs, per-row version isolation, structure-change full bump, and repeated-widgetId independence. Signed-off-by: Tim Mueller-Seydlitz <timbms@gmail.com>
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.
Summary
Fixes scroll hitching reported in #1136, introduced by the SwiftUI sitemap migration in 3.2.x.
Root cause: Every long-poll result unconditionally ran
reconcileWidgets+copyWidgetPropertiesand always reassigned@Published rowInputs, broadcastingobjectWillChangeon every cycle even when nothing the list renders changed. Under frequently updating items this produced continuous main-thread churn during inertial scrolling.Changes:
Snapshot before mutate —
updateUInow maps incoming page data toSitemapRowInputvalues before any widget object mutation. If the preview inputs equal the currentrowInputsand the title is unchanged, the method returns immediately — skipping all reconciliation and every@Publishedwrite. WhensearchTextis active, reconciliation still runs socurrentPagestays fresh and clearing the filter doesn't reveal stale hidden rows.RowID-keyed widget versioning —
widgetUpdateVersionsis now keyed byRowID.rawValue(pageKey + widgetId + occurrence) instead of barewidgetId. This prevents repeated widget IDs on one page from suppressing each other's version advances.bumpWidgetVersionsincrements by full row-identity pairs.SliderRowView,SegmentedRowView,SelectionRowView, andImageRowViewpassinput.rowIDto the version lookup; the four affected row-input types (SliderRowInput,SegmentedRowInput,SelectionRowInput,MediaRowInput) carry theirrowID.rowInputsequality guard —rebuildRowInputs(called on search-text changes and static init) skips the@Publishedassignment when the computed array equals the current one.Tests —
SitemapPageViewModelDiffingTestscovers: stable rebuild equality,objectWillChangesuppression on unchanged data, structure-change rebuild,bumpWidgetVersionsno-op on identical inputs, per-row version isolation, structure-change full bump, and repeated-widgetId independence (8 cases).Test plan
openHABTestsSwift/SitemapPageViewModelDiffingTests— all 8 cases passrefreshintervals — verify periodic reload still works