Skip to content

Fix sitemap scroll hitching under frequent long-poll updates#1138

Merged
timbms merged 1 commit intodevelopfrom
fix/sitemap-scroll-hitching-swiftui
Apr 12, 2026
Merged

Fix sitemap scroll hitching under frequent long-poll updates#1138
timbms merged 1 commit intodevelopfrom
fix/sitemap-scroll-hitching-swiftui

Conversation

@timbms
Copy link
Copy Markdown
Contributor

@timbms timbms commented Apr 11, 2026

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 + copyWidgetProperties and always reassigned @Published rowInputs, broadcasting objectWillChange on 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 mutateupdateUI now maps incoming page data to SitemapRowInput values before any widget object mutation. If the preview inputs equal the current rowInputs and the title is unchanged, the method returns immediately — skipping all reconciliation and every @Published write. When searchText is active, reconciliation still runs so currentPage stays fresh and clearing the filter doesn't reveal stale hidden rows.

  • RowID-keyed widget versioningwidgetUpdateVersions is now keyed by RowID.rawValue (pageKey + widgetId + occurrence) instead of bare widgetId. This prevents repeated widget IDs on one page from suppressing each other's version advances. bumpWidgetVersions increments by full row-identity pairs. SliderRowView, SegmentedRowView, SelectionRowView, and ImageRowView pass input.rowID to the version lookup; the four affected row-input types (SliderRowInput, SegmentedRowInput, SelectionRowInput, MediaRowInput) carry their rowID.

  • rowInputs equality guardrebuildRowInputs (called on search-text changes and static init) skips the @Published assignment when the computed array equals the current one.

  • TestsSitemapPageViewModelDiffingTests covers: 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 (8 cases).

Test plan

  • Build and run openHABTestsSwift/SitemapPageViewModelDiffingTests — all 8 cases pass
  • Manual: open a sitemap with frequently updating items (temperature sensors, power meters), scroll quickly — verify no stutter
  • Manual: use search field, scroll, clear search — verify rows reappear correctly with current state
  • Manual: sliders, segmented controls, selection rows — verify interactive controls still sync with server state
  • Manual: image/chart rows with refresh intervals — verify periodic reload still works

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>
@timbms timbms merged commit e02c298 into develop Apr 12, 2026
2 checks passed
@timbms timbms deleted the fix/sitemap-scroll-hitching-swiftui branch April 12, 2026 12:29
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant