From 59665f6940355f198be83dba215fd018d1cd1223 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:06:42 +0000 Subject: [PATCH 1/6] Initial plan From 2ff1841fb64a49b156cb9d81e42681bd9b53baea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:18:17 +0000 Subject: [PATCH 2/6] Fix memory leaks: unsubscribe missing event handlers in CameraSession, SearchBarHandler, and SlidableLayout Co-authored-by: Vetle444 <35739538+Vetle444@users.noreply.github.com> --- CHANGELOG.md | 5 +++++ .../DIPS.Mobile.UI/API/Camera/Shared/iOS/CameraSession.cs | 1 + .../Components/Searching/iOS/SearchBarHandler.cs | 2 +- .../DIPS.Mobile.UI/Components/Slidable/SlidableLayout.cs | 8 +++++--- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 566fc8355..8c9e965eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [55.2.3] +- [iOS][SearchBar] Fixed memory leak where `SearchButtonClicked` event was never unsubscribed due to wrong event name in cleanup +- [iOS][Camera] Fixed memory leak where `PreviewView.OnTapToFocus` event was never unsubscribed when stopping camera session +- [SlidableLayout] Fixed memory leak where `TapGestureRecognizer.Tapped` event was never unsubscribed due to missing field reference + ## [55.2.2] - [iOS26][Tip] Added more padding. diff --git a/src/library/DIPS.Mobile.UI/API/Camera/Shared/iOS/CameraSession.cs b/src/library/DIPS.Mobile.UI/API/Camera/Shared/iOS/CameraSession.cs index 5e663dbd2..ef573f33a 100644 --- a/src/library/DIPS.Mobile.UI/API/Camera/Shared/iOS/CameraSession.cs +++ b/src/library/DIPS.Mobile.UI/API/Camera/Shared/iOS/CameraSession.cs @@ -78,6 +78,7 @@ await Task.Run(() => if (PreviewView is not null) { PreviewView.OnZoomChanged -= PreviewViewOnZoomChanged; + PreviewView.OnTapToFocus -= PreviewViewOnOnTapToFocus; PreviewView?.Dispose(); PreviewView = null; } diff --git a/src/library/DIPS.Mobile.UI/Components/Searching/iOS/SearchBarHandler.cs b/src/library/DIPS.Mobile.UI/Components/Searching/iOS/SearchBarHandler.cs index 38e5d4699..527c2e7cf 100644 --- a/src/library/DIPS.Mobile.UI/Components/Searching/iOS/SearchBarHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Searching/iOS/SearchBarHandler.cs @@ -224,7 +224,7 @@ private void OnSearchButtonClicked(object? sender, EventArgs e) private void UnSubscribeToEvents(DuiSearchBar platformView) { platformView.CancelButtonClicked -= OnCancelButtonClicked; - platformView.CancelButtonClicked -= OnSearchButtonClicked; + platformView.SearchButtonClicked -= OnSearchButtonClicked; platformView.TextChanged -= OnSearchTextChanged; if (platformView.SearchTextField.ValueForKey(new NSString("_clearButton")) is UIButton clearButton) diff --git a/src/library/DIPS.Mobile.UI/Components/Slidable/SlidableLayout.cs b/src/library/DIPS.Mobile.UI/Components/Slidable/SlidableLayout.cs index 176708c4d..7ae8f6955 100644 --- a/src/library/DIPS.Mobile.UI/Components/Slidable/SlidableLayout.cs +++ b/src/library/DIPS.Mobile.UI/Components/Slidable/SlidableLayout.cs @@ -8,6 +8,7 @@ namespace DIPS.Mobile.UI.Components.Slidable public abstract partial class SlidableLayout : ContentView { private readonly PanGestureRecognizer m_panGestureRecognizer; + private readonly TapGestureRecognizer m_tapGestureRecognizer; private readonly AccelerationService m_accelerator = new(true); private int m_lastId = -2; // Different than default of SlideProperties private double m_startSlideLocation; @@ -37,9 +38,9 @@ public SlidableLayout() m_panGestureRecognizer.PanUpdated += PanGestureRecognizerPanUpdated; - var tapGestureRecognizer = new TapGestureRecognizer(); - GestureRecognizers.Add(tapGestureRecognizer); - tapGestureRecognizer.Tapped += OnEntireLayoutTapped; + m_tapGestureRecognizer = new TapGestureRecognizer(); + GestureRecognizers.Add(m_tapGestureRecognizer); + m_tapGestureRecognizer.Tapped += OnEntireLayoutTapped; m_currentOrientation = DeviceDisplay.MainDisplayInfo.Orientation; } @@ -296,6 +297,7 @@ protected override void OnHandlerChanging(HandlerChangingEventArgs args) if (args.NewHandler is null) { m_panGestureRecognizer.PanUpdated -= PanGestureRecognizerPanUpdated; + m_tapGestureRecognizer.Tapped -= OnEntireLayoutTapped; } } } From a8fb9568e0bec0e364e3dc5426c90343c1afd336 Mon Sep 17 00:00:00 2001 From: Vetle444 Date: Tue, 7 Apr 2026 15:48:31 +0200 Subject: [PATCH 3/6] more leak fix --- .../Gallery/BottomSheet/GalleryBottomSheet.cs | 68 ++++++++++--------- .../Alerting/SystemMessage/SystemMessage.cs | 1 + .../Android/BottomSheetHandler.cs | 19 +++++- .../Loading/Skeleton/SkeletonView.cs | 10 +++ .../Components/Loading/StateView/StateView.cs | 9 +++ .../FloatingNavigationButtonHandler.cs | 15 ++-- .../FloatingNavigationButtonService.cs | 1 + .../iOS/BaseDatePickerHandler.cs | 1 + .../Pickers/ItemPicker/ItemPicker.cs | 5 ++ .../BaseNullableDatePicker.cs | 3 + .../Android/ScrollPickerHandler.cs | 1 + .../SegmentedControl/SegmentedControl.cs | 40 +++++------ .../Components/Searching/SearchPage.cs | 5 +- .../DIPS.Mobile.UI/Components/Shell/Shell.cs | 10 +++ .../Components/TabView/iOS/TabView.cs | 16 +++-- 15 files changed, 140 insertions(+), 64 deletions(-) diff --git a/src/library/DIPS.Mobile.UI/API/Camera/Gallery/BottomSheet/GalleryBottomSheet.cs b/src/library/DIPS.Mobile.UI/API/Camera/Gallery/BottomSheet/GalleryBottomSheet.cs index 0b4d23631..39ae85d4a 100644 --- a/src/library/DIPS.Mobile.UI/API/Camera/Gallery/BottomSheet/GalleryBottomSheet.cs +++ b/src/library/DIPS.Mobile.UI/API/Camera/Gallery/BottomSheet/GalleryBottomSheet.cs @@ -144,18 +144,7 @@ private void GoToEditState() m_currentlyRotatedImageDisplayed.TranslationY -= m_topToolbar.HeightRequest; - m_currentlyRotatedImageDisplayed.SizeChanged += delegate - { - if (m_startingImageWidth is not null) - return; - - m_startingImageWidth = m_currentlyRotatedImageDisplayed.Width; - m_startingImageHeight = m_currentlyRotatedImageDisplayed.Height; - - m_carouselView.Opacity = 0; - m_navigatePreviousImageButton.IsVisible = false; - m_navigateNextImageButton.IsVisible = false; - }; + m_currentlyRotatedImageDisplayed.SizeChanged += OnRotatedImageSizeChanged; m_rotatingImageTcs = null; @@ -169,11 +158,25 @@ private void GoToDefaultState() m_positionBeforeEdit = m_carouselView!.Position; + m_currentlyRotatedImageDisplayed.SizeChanged -= OnRotatedImageSizeChanged; m_grid.Remove(m_currentlyRotatedImageDisplayed); UpdateNavigationButtonsVisibility(m_carouselView.Position); OnImagesChanged(); } + private void OnRotatedImageSizeChanged(object? sender, EventArgs e) + { + if (m_startingImageWidth is not null) + return; + + m_startingImageWidth = m_currentlyRotatedImageDisplayed.Width; + m_startingImageHeight = m_currentlyRotatedImageDisplayed.Height; + + m_carouselView!.Opacity = 0; + m_navigatePreviousImageButton.IsVisible = false; + m_navigateNextImageButton.IsVisible = false; + } + async void IGalleryDefaultStateObserver.RemoveImage() { if(Images.Count == 0) @@ -276,6 +279,7 @@ private void OnImagesChanged() if (m_carouselView is not null) { + m_carouselView.SizeChanged -= OnCarouselViewSizeChanged; m_carouselView.PositionChanged -= CarouselViewOnPositionChanged; try { @@ -310,30 +314,32 @@ private void OnImagesChanged() }; m_carouselViewWrapperView.Content = m_carouselView; - m_carouselView.SizeChanged += delegate - { - if (m_hasSetToolbarHeights) - { - return; - } - - var actualImageHeight = m_carouselView.Width / CameraPreview.ThreeFourRatio; - var letterBoxHeight = m_carouselView.Height - actualImageHeight; - - m_topToolbar.HeightRequest = letterBoxHeight * CameraPreview.TopToolbarPercentHeightOfLetterBox; - m_bottomToolbar.HeightRequest = letterBoxHeight * CameraPreview.BottomToolbarPercentHeightOfLetterBox; - - m_carouselViewWrapperView.TranslationY -= m_topToolbar.HeightRequest; - m_navigatePreviousImageButton.TranslationY -= m_topToolbar.HeightRequest; - m_navigateNextImageButton.TranslationY -= m_topToolbar.HeightRequest; - - m_hasSetToolbarHeights = true; - }; + m_carouselView.SizeChanged += OnCarouselViewSizeChanged; m_carouselView.PositionChanged += CarouselViewOnPositionChanged; m_grid.Insert(0, m_carouselViewWrapperView); } + private void OnCarouselViewSizeChanged(object? sender, EventArgs e) + { + if (m_hasSetToolbarHeights) + { + return; + } + + var actualImageHeight = m_carouselView!.Width / CameraPreview.ThreeFourRatio; + var letterBoxHeight = m_carouselView.Height - actualImageHeight; + + m_topToolbar.HeightRequest = letterBoxHeight * CameraPreview.TopToolbarPercentHeightOfLetterBox; + m_bottomToolbar.HeightRequest = letterBoxHeight * CameraPreview.BottomToolbarPercentHeightOfLetterBox; + + m_carouselViewWrapperView.TranslationY -= m_topToolbar.HeightRequest; + m_navigatePreviousImageButton.TranslationY -= m_topToolbar.HeightRequest; + m_navigateNextImageButton.TranslationY -= m_topToolbar.HeightRequest; + + m_hasSetToolbarHeights = true; + } + private void OnStartingIndexChanged() { if(m_carouselView is not null) diff --git a/src/library/DIPS.Mobile.UI/Components/Alerting/SystemMessage/SystemMessage.cs b/src/library/DIPS.Mobile.UI/Components/Alerting/SystemMessage/SystemMessage.cs index 9b4e8ade2..2b3841968 100644 --- a/src/library/DIPS.Mobile.UI/Components/Alerting/SystemMessage/SystemMessage.cs +++ b/src/library/DIPS.Mobile.UI/Components/Alerting/SystemMessage/SystemMessage.cs @@ -125,5 +125,6 @@ public void Dispose() { m_timer.Stop(); m_timer.Elapsed -= OnTimerEnded; + m_timer.Dispose(); } } \ No newline at end of file diff --git a/src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetHandler.cs b/src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetHandler.cs index 3e5d0ab41..7d658e845 100644 --- a/src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetHandler.cs @@ -39,6 +39,8 @@ public partial class BottomSheetHandler : ContentViewHandler private BottomSheetHeader m_bottomSheetHeader; private List> m_weakSearchBars = []; private WeakReference? m_weakCurrentFocusedSearchBar; + private BottomSheetCallback? m_bottomSheetCallback; + private KeyListener? m_keyListener; public AView OnBeforeOpening(IMauiContext mauiContext, Context context, AView bottomSheetAndroidView, RelativeLayout rootLayout, LinearLayout bottomSheetLayout) @@ -46,10 +48,11 @@ public AView OnBeforeOpening(IMauiContext mauiContext, Context context, AView bo if (VirtualView is not BottomSheet bottomSheet) return new AView(Context); m_bottomSheet = bottomSheet; - bottomSheet.BottomSheetDialog.Behavior.AddBottomSheetCallback( - new BottomSheetCallback(this)); + m_bottomSheetCallback = new BottomSheetCallback(this); + m_keyListener = new KeyListener(this); + bottomSheet.BottomSheetDialog.Behavior.AddBottomSheetCallback(m_bottomSheetCallback); bottomSheet.BottomSheetDialog.SetOnShowListener(new DialogInterfaceOnShowListener(this)); - bottomSheet.BottomSheetDialog.SetOnKeyListener(new KeyListener(this)); + bottomSheet.BottomSheetDialog.SetOnKeyListener(m_keyListener); //Add a handle, with a innerGrid that works as a big hit box for the user to hit //Inspired by com.google.android.material.bottomsheet.BottomSheetDragHandleView , which will be added in Xamarin Android Material Design v1.7.0. https://github.com/material-components/material-components-android/commit/ac7b761294808748df167b50b223b591ca9dac06 @@ -234,6 +237,16 @@ protected override void DisconnectHandler(ContentViewGroup platformView) { base.DisconnectHandler(platformView); + if (m_bottomSheetCallback is not null) + { + m_bottomSheet.BottomSheetDialog.Behavior.RemoveBottomSheetCallback(m_bottomSheetCallback); + m_bottomSheetCallback = null; + } + + m_bottomSheet.BottomSheetDialog.SetOnShowListener(null); + m_bottomSheet.BottomSheetDialog.SetOnKeyListener(null); + m_keyListener = null; + s_mEmptyNonFitToContentView?.RemoveFromParent(); m_bottomSheetHeader.DisconnectHandlers(); diff --git a/src/library/DIPS.Mobile.UI/Components/Loading/Skeleton/SkeletonView.cs b/src/library/DIPS.Mobile.UI/Components/Loading/Skeleton/SkeletonView.cs index e458ff066..26449101a 100644 --- a/src/library/DIPS.Mobile.UI/Components/Loading/Skeleton/SkeletonView.cs +++ b/src/library/DIPS.Mobile.UI/Components/Loading/Skeleton/SkeletonView.cs @@ -134,6 +134,16 @@ private BoxView CreateBox(SkeletonShape shape) return box; } + protected override void OnHandlerChanging(HandlerChangingEventArgs args) + { + base.OnHandlerChanging(args); + + if (args.NewHandler is not null) + return; + + StopAnimation(); + } + private void StartAnimation() { StopAnimation(); diff --git a/src/library/DIPS.Mobile.UI/Components/Loading/StateView/StateView.cs b/src/library/DIPS.Mobile.UI/Components/Loading/StateView/StateView.cs index 94151038c..e252fef42 100644 --- a/src/library/DIPS.Mobile.UI/Components/Loading/StateView/StateView.cs +++ b/src/library/DIPS.Mobile.UI/Components/Loading/StateView/StateView.cs @@ -120,11 +120,20 @@ private async Task FadeIn(IView? view) } } + private StateViewModel? m_previousStateViewModel; + private void OnStateViewModelChanged() { if(StateViewModel is null) return; + if (m_previousStateViewModel is not null) + { + m_previousStateViewModel.OnStateChanged -= OnStateChanged; + } + + m_previousStateViewModel = StateViewModel; + ErrorView ??= new ErrorView { BindingContext = StateViewModel.Error }; LoadingView ??= new LoadingView { BindingContext = StateViewModel.Loading }; EmptyView ??= new EmptyView { BindingContext = StateViewModel.Empty }; diff --git a/src/library/DIPS.Mobile.UI/Components/Navigation/FloatingNavigationButton/Android/FloatingNavigationButtonHandler.cs b/src/library/DIPS.Mobile.UI/Components/Navigation/FloatingNavigationButton/Android/FloatingNavigationButtonHandler.cs index 69c6b1608..d4965ec3a 100644 --- a/src/library/DIPS.Mobile.UI/Components/Navigation/FloatingNavigationButton/Android/FloatingNavigationButtonHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Navigation/FloatingNavigationButton/Android/FloatingNavigationButtonHandler.cs @@ -11,17 +11,24 @@ private static partial void MapIsClickable(FloatingNavigationButtonHandler handl if (handler.PlatformView is not global::Android.Views.View aView) return; if (handler.VirtualView is not FloatingNavigationButton fab) return; + aView.Click -= handler.OnNativeViewClick; + if (floatingNavigationButton.IsClickable) { aView.Clickable = true; - aView.Click += (_, _) => - { - _ = fab.Close(); - }; + aView.Click += handler.OnNativeViewClick; } else { aView.Clickable = false; } } + + private void OnNativeViewClick(object? sender, EventArgs e) + { + if (VirtualView is FloatingNavigationButton fab) + { + _ = fab.Close(); + } + } } \ No newline at end of file diff --git a/src/library/DIPS.Mobile.UI/Components/Navigation/FloatingNavigationButton/FloatingNavigationButtonService.cs b/src/library/DIPS.Mobile.UI/Components/Navigation/FloatingNavigationButton/FloatingNavigationButtonService.cs index 7214e5270..93921361e 100644 --- a/src/library/DIPS.Mobile.UI/Components/Navigation/FloatingNavigationButton/FloatingNavigationButtonService.cs +++ b/src/library/DIPS.Mobile.UI/Components/Navigation/FloatingNavigationButton/FloatingNavigationButtonService.cs @@ -150,6 +150,7 @@ public static void Close() public static void Remove() { PlatformRemove(); + FloatingNavigationButton = null; } private static partial void PlatformRemove(); diff --git a/src/library/DIPS.Mobile.UI/Components/Pickers/DatePickerShared/iOS/BaseDatePickerHandler.cs b/src/library/DIPS.Mobile.UI/Components/Pickers/DatePickerShared/iOS/BaseDatePickerHandler.cs index a3b000b3d..93f610fee 100644 --- a/src/library/DIPS.Mobile.UI/Components/Pickers/DatePickerShared/iOS/BaseDatePickerHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Pickers/DatePickerShared/iOS/BaseDatePickerHandler.cs @@ -63,6 +63,7 @@ protected override void DisconnectHandler(UIDatePicker platformView) { base.DisconnectHandler(platformView); + platformView.ValueChanged -= OnValueChanged; platformView.EditingDidBegin -= OnOpen; platformView.EditingDidEnd -= OnClose; diff --git a/src/library/DIPS.Mobile.UI/Components/Pickers/ItemPicker/ItemPicker.cs b/src/library/DIPS.Mobile.UI/Components/Pickers/ItemPicker/ItemPicker.cs index 1786bd7be..3941535cf 100644 --- a/src/library/DIPS.Mobile.UI/Components/Pickers/ItemPicker/ItemPicker.cs +++ b/src/library/DIPS.Mobile.UI/Components/Pickers/ItemPicker/ItemPicker.cs @@ -154,6 +154,11 @@ private static void ItemsSourceChanged(BindableObject bindable, object oldValue, picker.AddContextMenuItems(); } + if (oldValue is INotifyCollectionChanged oldNotifyCollectionChanged) + { + oldNotifyCollectionChanged.CollectionChanged -= picker.OnCollectionChanged; + } + if (newValue is INotifyCollectionChanged notifyCollectionChanged) { notifyCollectionChanged.CollectionChanged += picker.OnCollectionChanged; diff --git a/src/library/DIPS.Mobile.UI/Components/Pickers/NullableDatePickerShared/BaseNullableDatePicker.cs b/src/library/DIPS.Mobile.UI/Components/Pickers/NullableDatePickerShared/BaseNullableDatePicker.cs index 8a41d76c2..e0845afed 100644 --- a/src/library/DIPS.Mobile.UI/Components/Pickers/NullableDatePickerShared/BaseNullableDatePicker.cs +++ b/src/library/DIPS.Mobile.UI/Components/Pickers/NullableDatePickerShared/BaseNullableDatePicker.cs @@ -27,7 +27,10 @@ protected override void OnHandlerChanging(HandlerChangingEventArgs args) base.OnHandlerChanging(args); if(args.NewHandler is null) + { + DateEnabledSwitch.Toggled -= OnSwitchToggled; return; + } m_dateOrTimePicker = CreateDateOrTimePicker(); m_dateOrTimePicker.IsVisible = false; diff --git a/src/library/DIPS.Mobile.UI/Components/Pickers/ScrollPicker/Android/ScrollPickerHandler.cs b/src/library/DIPS.Mobile.UI/Components/Pickers/ScrollPicker/Android/ScrollPickerHandler.cs index 740beecc8..227d87947 100644 --- a/src/library/DIPS.Mobile.UI/Components/Pickers/ScrollPicker/Android/ScrollPickerHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/Pickers/ScrollPicker/Android/ScrollPickerHandler.cs @@ -72,6 +72,7 @@ protected override void DisconnectHandler(Chip platformView) platformView.Click -= PlatformViewOnClick; m_scrollPickerViewModel.OnAnyComponentsDataInvalidated -= SetChipTitle; + m_scrollPickerViewModel.Dispose(); } } diff --git a/src/library/DIPS.Mobile.UI/Components/Pickers/SegmentedControl/SegmentedControl.cs b/src/library/DIPS.Mobile.UI/Components/Pickers/SegmentedControl/SegmentedControl.cs index 863dd5c93..8453f3113 100644 --- a/src/library/DIPS.Mobile.UI/Components/Pickers/SegmentedControl/SegmentedControl.cs +++ b/src/library/DIPS.Mobile.UI/Components/Pickers/SegmentedControl/SegmentedControl.cs @@ -95,32 +95,34 @@ private View CreateSegment() horizontalStackLayout.Add(checkedImage); horizontalStackLayout.Add(label); border.Content = horizontalStackLayout; - border.SizeChanged += ((sender, _) => - { - if (sender is not View view) return; + border.SizeChanged += OnBorderSizeChanged; - // Sometimes on Android, the different does not have equal heights, so this is a workaround to ensure all borders have same height. + return border; + } + + private void OnBorderSizeChanged(object? sender, EventArgs _) + { + if (sender is not Border border) return; + + // Sometimes on Android, the different does not have equal heights, so this is a workaround to ensure all borders have same height. #if __ANDROID__ border.HeightRequest = this.Height; #endif - if (view.BindingContext is not SelectableItemViewModel selectableListItem) return; + if (border.BindingContext is not SelectableItemViewModel selectableListItem) return; - var radius = Sizes.GetSize(SizeName.radius_xlarge); - var roundRectangle = new RoundRectangle() { StrokeThickness = 0}; - if (m_allSelectableItems.Last() == selectableListItem) - { - roundRectangle.CornerRadius = new CornerRadius(0, radius, 0, radius); - } - else if (m_allSelectableItems.First() == selectableListItem) - { - roundRectangle.CornerRadius = new CornerRadius(radius, 0, radius, 0); - } - - border.StrokeShape = roundRectangle; - }); + var radius = Sizes.GetSize(SizeName.radius_xlarge); + var roundRectangle = new RoundRectangle() { StrokeThickness = 0}; + if (m_allSelectableItems.Last() == selectableListItem) + { + roundRectangle.CornerRadius = new CornerRadius(0, radius, 0, radius); + } + else if (m_allSelectableItems.First() == selectableListItem) + { + roundRectangle.CornerRadius = new CornerRadius(radius, 0, radius, 0); + } - return border; + border.StrokeShape = roundRectangle; } private void OnItemTouched(SelectableItemViewModel selectableItemViewModel) diff --git a/src/library/DIPS.Mobile.UI/Components/Searching/SearchPage.cs b/src/library/DIPS.Mobile.UI/Components/Searching/SearchPage.cs index 5b9717ee7..c799059b5 100644 --- a/src/library/DIPS.Mobile.UI/Components/Searching/SearchPage.cs +++ b/src/library/DIPS.Mobile.UI/Components/Searching/SearchPage.cs @@ -46,8 +46,6 @@ public SearchPage() SearchBar.SetBinding(SearchBar.DelayProperty, static (SearchPage searchPage) => searchPage.Delay, source: this); SearchBar.SetBinding(SearchBar.IsAutocorrectEnabledProperty, static (SearchPage searchPage) => searchPage.IsAutocorrectEnabled, source: this); - SearchBar.TextChanged += SearchBarOnTextChanged; - SearchBar.SearchCommand = new Command(() => OnSearchQueryChanged(SearchBar.Text)); SearchBar.ClearTextCommand = new Command(TextWasClearedFromClick); SearchBar.CancelCommand = CancelCommand; @@ -125,6 +123,7 @@ protected override void OnHandlerChanged() SearchBar.Focus(); } + SearchBar.TextChanged += SearchBarOnTextChanged; SearchBar.Focused += OnSearchBarFocused; } @@ -209,7 +208,6 @@ private async void OnSearchQueryChanged(string searchQuery) protected override void OnDisappearing() { - SearchBar.TextChanged -= SearchBarOnTextChanged; SearchBar.Unfocus(); base.OnDisappearing(); } @@ -279,6 +277,7 @@ protected override void OnHandlerChanging(HandlerChangingEventArgs args) if (args.NewHandler is null) { + SearchBar.TextChanged -= SearchBarOnTextChanged; m_resultCollectionView.Scrolled -= OnCollectionViewScrolled; SearchBar.Focused -= OnSearchBarFocused; } diff --git a/src/library/DIPS.Mobile.UI/Components/Shell/Shell.cs b/src/library/DIPS.Mobile.UI/Components/Shell/Shell.cs index 26dea9cea..5433de96e 100644 --- a/src/library/DIPS.Mobile.UI/Components/Shell/Shell.cs +++ b/src/library/DIPS.Mobile.UI/Components/Shell/Shell.cs @@ -35,6 +35,16 @@ public Shell() SetNavBarHasShadow(this, false); } + protected override void OnHandlerChanging(HandlerChangingEventArgs args) + { + base.OnHandlerChanging(args); + + if (args.NewHandler is not null) + return; + + Navigated -= OnNavigated; + } + private async void OnNavigated(object? sender, ShellNavigatedEventArgs e) { switch (e.Source) diff --git a/src/library/DIPS.Mobile.UI/Components/TabView/iOS/TabView.cs b/src/library/DIPS.Mobile.UI/Components/TabView/iOS/TabView.cs index 963150cb9..2cd5d1bbc 100644 --- a/src/library/DIPS.Mobile.UI/Components/TabView/iOS/TabView.cs +++ b/src/library/DIPS.Mobile.UI/Components/TabView/iOS/TabView.cs @@ -44,10 +44,7 @@ private void ItemsSourceChanged() list.ForEach(obj => { var tab = new Tab() { Title = obj.Title, Counter = obj.Counter }; - tab.Tapped += (sender, args) => - { - _ = TabToggled(tab); - }; + tab.Tapped += OnTabTapped; var item = tab; @@ -105,8 +102,19 @@ private void SetTextStyleForAllTabs() } } + private void OnTabTapped(object? sender, EventArgs args) + { + if (sender is Tab tab) + _ = TabToggled(tab); + } + private void ClearItems() { + foreach (var tab in m_tabItems) + { + tab.Tapped -= OnTabTapped; + } + m_tabItems.Clear(); m_stackLayout.Clear(); m_previouslySelectedTabIndex = -1; From baa23d9cf6cf602126062fa6b703d7d3d2dc8e7e Mon Sep 17 00:00:00 2001 From: Vetle444 Date: Tue, 7 Apr 2026 16:02:37 +0200 Subject: [PATCH 4/6] fix pipeline --- build/bootstrapper/bootstrapper.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/bootstrapper/bootstrapper.sh b/build/bootstrapper/bootstrapper.sh index 5904d7d8e..de0272971 100755 --- a/build/bootstrapper/bootstrapper.sh +++ b/build/bootstrapper/bootstrapper.sh @@ -60,7 +60,7 @@ fi #azure-CLI -if az > /dev/null ; then +if az > /dev/null 2>&1 ; then echo "✅ Azure CLI was found" else echo "❌ Azure CLI not found, installing it..." From df0836d668bb91c899f74916aa8ed5b03b115183 Mon Sep 17 00:00:00 2001 From: Vetle444 Date: Wed, 8 Apr 2026 11:59:30 +0200 Subject: [PATCH 5/6] changelog --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 046ca6beb..5a6640b9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,5 @@ ## [55.6.4] -- [iOS][SearchBar] Fixed memory leak where `SearchButtonClicked` event was never unsubscribed due to wrong event name in cleanup -- [iOS][Camera] Fixed memory leak where `PreviewView.OnTapToFocus` event was never unsubscribed when stopping camera session -- [SlidableLayout] Fixed memory leak where `TapGestureRecognizer.Tapped` event was never unsubscribed due to missing field reference +- Fixed multiple memory leaks across components: unsubscribed events in `BottomSheetHandler`, `BaseDatePickerHandler`, `BaseNullableDatePicker`, `TabView`, `Shell`, `SkeletonView`, `SegmentedControl`, `StateView`, `ItemPicker`, `SearchPage`, `ScrollPickerHandler`, `FloatingNavigationButton`, `GalleryBottomSheet`, and `SystemMessage` ## [55.6.3] - [ScrollView][iOS] ShouldBounce now sets correct native property. From 235d9dd2f3ee0a454bbd2870c3989320b5bee647 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:04:56 +0000 Subject: [PATCH 6/6] Fix: unsubscribe border.SizeChanged in SegmentedControl when items are replaced or handler disconnects Agent-Logs-Url: https://github.com/DIPSAS/DIPS.Mobile.UI/sessions/0ff7e0eb-4f90-4086-82a6-b6528e2a7d38 Co-authored-by: Vetle444 <35739538+Vetle444@users.noreply.github.com> --- .../SegmentedControl/SegmentedControl.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/library/DIPS.Mobile.UI/Components/Pickers/SegmentedControl/SegmentedControl.cs b/src/library/DIPS.Mobile.UI/Components/Pickers/SegmentedControl/SegmentedControl.cs index 8453f3113..133e12d87 100644 --- a/src/library/DIPS.Mobile.UI/Components/Pickers/SegmentedControl/SegmentedControl.cs +++ b/src/library/DIPS.Mobile.UI/Components/Pickers/SegmentedControl/SegmentedControl.cs @@ -160,9 +160,23 @@ private void SendDidSelect(object item) SelectedItemCommand?.Execute(item); } + private void UnsubscribeBorderEvents() + { + foreach (var child in m_horizontalStackLayout.Children) + { + if (child is Border border) + { + border.SizeChanged -= OnBorderSizeChanged; + } + } + } + private void ItemsSourceChanged() { if (ItemsSource == null) return; + + UnsubscribeBorderEvents(); + var listOfSelectableItems = new List(); foreach (var item in ItemsSource.Cast().ToList()) @@ -185,4 +199,14 @@ private void ItemsSourceChanged() m_allSelectableItems = listOfSelectableItems; BindableLayout.SetItemsSource(m_horizontalStackLayout, m_allSelectableItems); } + + protected override void OnHandlerChanging(HandlerChangingEventArgs args) + { + base.OnHandlerChanging(args); + + if (args.NewHandler is null) + { + UnsubscribeBorderEvents(); + } + } } \ No newline at end of file