Skip to content

SwipeView leaks when SwipeItems are reused or replaced #35481

Description

@AdamEssenmacher

Description

SwipeView.OnSwipeItemsChanged subscribes local-function handlers to SwipeItems.CollectionChanged and SwipeItems.PropertyChanged. Those local functions capture the owning SwipeView.

When LeftItems, RightItems, TopItems, or BottomItems later changes, MAUI attempts to unsubscribe from the old SwipeItems, but it recreates new local-function delegates during the new property-change callback. Those delegates do not match the originally subscribed delegates, so the old SwipeItems continues to hold the original handlers.

If the old SwipeItems is long-lived, for example cached, reused from a helper, or stored as a resource, it retains the old SwipeView, its binding context, row view models, and page-level data after the page is closed.

The repro project demonstrates this with a dispatch-board style page containing swipeable rows. On iOS, the control run releases all rows after full GC, while the cached SwipeItems run retains 1000/1000 SwipeViews and 125 MB of simulated row payload. Replacing RightItems during page disappearance still leaks, which demonstrates the failed unsubscribe path.

Steps to Reproduce

  1. Clone the repro branch:

    git clone https://github.com/AdamEssenmacher/maui.git
    cd maui
    git checkout repro/swipeitems-retention-leak
  2. Build the MAUI build tasks:

    dotnet build Microsoft.Maui.BuildTasks.slnf
  3. Run the repro project on iOS, Mac Catalyst, or Android:

    dotnet run --project src/Controls/samples/SwipeItemsLeakRepro/SwipeItemsLeakRepro.csproj -f net10.0-maccatalyst

    Or:

    dotnet build src/Controls/samples/SwipeItemsLeakRepro/SwipeItemsLeakRepro.csproj -f net10.0-ios -p:RuntimeIdentifier=iossimulator-arm64 -t:Run
    dotnet build src/Controls/samples/SwipeItemsLeakRepro/SwipeItemsLeakRepro.csproj -f net10.0-android -t:Run
  4. In the app, leave the default settings:

    Pages/run: 25
    Swipe rows/page: 40
    Payload KB/row: 128
    Dwell ms/page: 40
    
  5. Tap Run control.

    • Expected: after forced GC, retained weak refs should be near zero.
  6. Tap Run cached SwipeItems.

    • Actual on unpatched builds: old SwipeViews, row view models, and board view models remain alive after full GC.
  7. Tap Run replace RightItems.

    • Actual on unpatched builds: replacing each row’s RightItems on page disappearance still does not release the old SwipeItems subscriptions.

Observed iOS result:

Run control:
SwipeViews: 0/1000
row view models: 0/1000
retained payload: 0 B

Run cached SwipeItems:
SwipeViews: 1000/1000
row view models: 1000/1000
board view models: 25/25
retained payload: 125.0 MB

Observed Android result:

Run control:
SwipeViews: 120/1000
row view models: 120/1000
retained payload: 15.0 MB

Run cached SwipeItems:
SwipeViews: 1000/1000
row view models: 1000/1000
board view models: 25/25
retained payload: 125.0 MB

Link to public reproduction project repository

SwipeItemsLeakRepro

Version with bug

10.0.60

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

Unknown/Other

Affected platforms

Android, macOS, iOS, I was not able test on other platforms

Affected platform versions

No response

Did you find any workaround?

No response

Relevant log output

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions