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
-
Clone the repro branch:
git clone https://github.com/AdamEssenmacher/maui.git
cd maui
git checkout repro/swipeitems-retention-leak
-
Build the MAUI build tasks:
dotnet build Microsoft.Maui.BuildTasks.slnf
-
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
-
In the app, leave the default settings:
Pages/run: 25
Swipe rows/page: 40
Payload KB/row: 128
Dwell ms/page: 40
-
Tap Run control.
- Expected: after forced GC, retained weak refs should be near zero.
-
Tap Run cached SwipeItems.
- Actual on unpatched builds: old
SwipeViews, row view models, and board view models remain alive after full GC.
-
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
Description
SwipeView.OnSwipeItemsChangedsubscribes local-function handlers toSwipeItems.CollectionChangedandSwipeItems.PropertyChanged. Those local functions capture the owningSwipeView.When
LeftItems,RightItems,TopItems, orBottomItemslater changes, MAUI attempts to unsubscribe from the oldSwipeItems, but it recreates new local-function delegates during the new property-change callback. Those delegates do not match the originally subscribed delegates, so the oldSwipeItemscontinues to hold the original handlers.If the old
SwipeItemsis long-lived, for example cached, reused from a helper, or stored as a resource, it retains the oldSwipeView, 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
SwipeItemsrun retains1000/1000SwipeViews and125 MBof simulated row payload. ReplacingRightItemsduring page disappearance still leaks, which demonstrates the failed unsubscribe path.Steps to Reproduce
Clone the repro branch:
git clone https://github.com/AdamEssenmacher/maui.git cd maui git checkout repro/swipeitems-retention-leakBuild the MAUI build tasks:
Run the repro project on iOS, Mac Catalyst, or Android:
Or:
In the app, leave the default settings:
Tap
Run control.Tap
Run cached SwipeItems.SwipeViews, row view models, and board view models remain alive after full GC.Tap
Run replace RightItems.RightItemson page disappearance still does not release the oldSwipeItemssubscriptions.Observed iOS result:
Observed Android result:
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