diff --git a/src/Controls/src/Core/SwipeView/SwipeItem.cs b/src/Controls/src/Core/SwipeView/SwipeItem.cs
index c295bc49362d..8505094c9eb6 100644
--- a/src/Controls/src/Core/SwipeView/SwipeItem.cs
+++ b/src/Controls/src/Core/SwipeView/SwipeItem.cs
@@ -14,7 +14,7 @@ public partial class SwipeItem : MenuItem, Controls.ISwipeItem, Maui.ISwipeItemM
public static readonly BindableProperty BackgroundColorProperty = BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(SwipeItem), null);
/// Bindable property for .
- public static readonly BindableProperty IsVisibleProperty = BindableProperty.Create(nameof(IsVisible), typeof(bool), typeof(SwipeItem), true);
+ public static readonly BindableProperty IsVisibleProperty = BindableProperty.Create(nameof(IsVisible), typeof(bool), typeof(SwipeItem), true, propertyChanged: OnIsVisibleChanged);
///
/// Gets or sets the background color of the swipe item. This is a bindable property.
@@ -40,6 +40,12 @@ public bool IsVisible
Visibility ISwipeItemMenuItem.Visibility => this.IsVisible ? Visibility.Visible : Visibility.Collapsed;
+ static void OnIsVisibleChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ var swipeItem = (SwipeItem)bindable;
+ swipeItem.Handler?.UpdateValue(nameof(ISwipeItemMenuItem.Visibility));
+ }
+
void Maui.ISwipeItem.OnInvoked()
{
if (Command != null && Command.CanExecute(CommandParameter))
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_BecomeVisible.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_BecomeVisible.png
new file mode 100644
index 000000000000..43f9ed3adb16
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_BecomeVisible.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_DeleteHidden.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_DeleteHidden.png
new file mode 100644
index 000000000000..318dc99a484e
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_DeleteHidden.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_DeleteVisible.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_DeleteVisible.png
new file mode 100644
index 000000000000..8959408950e6
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_DeleteVisible.png differ
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_InitiallyHidden.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_InitiallyHidden.png
new file mode 100644
index 000000000000..92b42ecd2533
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/SwipeOpen_InitiallyHidden.png differ
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue34832.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue34832.cs
new file mode 100644
index 000000000000..1fbc8e8dd37b
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue34832.cs
@@ -0,0 +1,108 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, 34832, "SwipeItem.IsVisible doesn't properly refresh native swipe items when binding value changes dynamically", PlatformAffected.Android | PlatformAffected.iOS)]
+public class Issue34832 : ContentPage
+{
+ readonly Issue34832ViewModel _viewModel = new() { IsDeleteVisible = false };
+ SwipeView _swipeView;
+
+ public Issue34832()
+ {
+ BindingContext = _viewModel;
+
+ SwipeItem deleteSwipeItem = new SwipeItem
+ {
+ Text = "Delete",
+ BackgroundColor = Colors.Green,
+ AutomationId = "DeleteSwipeItem"
+ };
+ deleteSwipeItem.SetBinding(SwipeItem.IsVisibleProperty, new Binding(nameof(Issue34832ViewModel.IsDeleteVisible)));
+
+ SwipeItem archiveSwipeItem = new SwipeItem
+ {
+ Text = "Archive",
+ BackgroundColor = Colors.Blue,
+ AutomationId = "ArchiveSwipeItem"
+ };
+
+ _swipeView = new SwipeView
+ {
+ AutomationId = "TestSwipeView",
+ HeightRequest = 60,
+ LeftItems = new SwipeItems { deleteSwipeItem, archiveSwipeItem },
+ Content = new Grid
+ {
+ BackgroundColor = Colors.LightGray,
+ Children =
+ {
+ new Label
+ {
+ Text = "Swipe left to reveal items",
+ HorizontalOptions = LayoutOptions.Center,
+ VerticalOptions = LayoutOptions.Center
+ }
+ }
+ }
+ };
+
+ Button toggleButton = new Button
+ {
+ Text = "Toggle Delete Visibility",
+ AutomationId = "ToggleVisibilityButton"
+ };
+ toggleButton.Clicked += (s, e) => _viewModel.IsDeleteVisible = !_viewModel.IsDeleteVisible;
+
+ Button openSwipeButton = new Button
+ {
+ Text = "Open Swipe",
+ AutomationId = "OpenSwipeButton"
+ };
+ openSwipeButton.Clicked += (s, e) => _swipeView?.Open(OpenSwipeItem.LeftItems);
+
+ Button resetButton = new Button
+ {
+ Text = "Reset",
+ AutomationId = "ResetButton"
+ };
+ resetButton.Clicked += (s, e) => _viewModel.IsDeleteVisible = false;
+
+ Content = new VerticalStackLayout
+ {
+ Padding = new Thickness(20),
+ Spacing = 20,
+ Children =
+ {
+ _swipeView,
+ toggleButton,
+ openSwipeButton,
+ resetButton,
+ }
+ };
+ }
+}
+
+public class Issue34832ViewModel : INotifyPropertyChanged
+{
+ bool _isDeleteVisible;
+
+ public bool IsDeleteVisible
+ {
+ get => _isDeleteVisible;
+ set
+ {
+ if (_isDeleteVisible != value)
+ {
+ _isDeleteVisible = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void OnPropertyChanged([CallerMemberName] string name = null) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+}
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_BecomeVisible.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_BecomeVisible.png
new file mode 100644
index 000000000000..8cce5d4ad741
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_BecomeVisible.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_DeleteHidden.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_DeleteHidden.png
new file mode 100644
index 000000000000..782676695ec2
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_DeleteHidden.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_DeleteVisible.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_DeleteVisible.png
new file mode 100644
index 000000000000..713a19f2dfdc
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_DeleteVisible.png differ
diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_InitiallyHidden.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_InitiallyHidden.png
new file mode 100644
index 000000000000..d76d5bd7f7e9
Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/SwipeOpen_InitiallyHidden.png differ
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34832.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34832.cs
new file mode 100644
index 000000000000..49b0f807aa3e
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue34832.cs
@@ -0,0 +1,65 @@
+#if TEST_FAILS_ON_WINDOWS // Issue Link - https://github.com/dotnet/maui/issues/35216
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+
+public class Issue34832 : _IssuesUITest
+{
+ public override string Issue => "SwipeItem.IsVisible doesn't properly refresh native swipe items when binding value changes dynamically";
+
+ public Issue34832(TestDevice device) : base(device)
+ {
+ }
+
+ [Test]
+ [Order(1)]
+ [Category(UITestCategories.SwipeView)]
+ public void SwipeItemInitiallyHiddenBecomesVisibleAfterBindingChanges()
+ {
+ Exception? exception = null;
+ App.WaitForElement("OpenSwipeButton");
+ App.Tap("OpenSwipeButton");
+
+ VerifyScreenshotOrSetException(ref exception, "SwipeOpen_InitiallyHidden");
+
+ App.Tap("ToggleVisibilityButton");
+
+ VerifyScreenshotOrSetException(ref exception, "SwipeOpen_BecomeVisible");
+
+ App.Tap("TestSwipeView");
+ App.Tap("ResetButton");
+
+ if (exception is not null)
+ {
+ throw exception;
+ }
+ }
+
+ [Test]
+ [Order(2)]
+ [Category(UITestCategories.SwipeView)]
+ public void SwipeItemBecomesHiddenAfterBindingChanges()
+ {
+ Exception? exception = null;
+ App.WaitForElement("ToggleVisibilityButton");
+ App.Tap("ToggleVisibilityButton");
+ App.Tap("OpenSwipeButton");
+
+ VerifyScreenshotOrSetException(ref exception, "SwipeOpen_DeleteVisible");
+
+ App.Tap("ToggleVisibilityButton");
+
+ VerifyScreenshotOrSetException(ref exception, "SwipeOpen_DeleteHidden");
+
+ App.Tap("TestSwipeView");
+ App.Tap("ResetButton");
+
+ if (exception is not null)
+ {
+ throw exception;
+ }
+ }
+}
+#endif
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_BecomeVisible.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_BecomeVisible.png
new file mode 100644
index 000000000000..3bcaf3537dc5
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_BecomeVisible.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_DeleteHidden.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_DeleteHidden.png
new file mode 100644
index 000000000000..ea99fabe91ac
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_DeleteHidden.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_DeleteVisible.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_DeleteVisible.png
new file mode 100644
index 000000000000..8d33df1339f1
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_DeleteVisible.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_InitiallyHidden.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_InitiallyHidden.png
new file mode 100644
index 000000000000..f0b0413da129
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/SwipeOpen_InitiallyHidden.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_BecomeVisible.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_BecomeVisible.png
new file mode 100644
index 000000000000..576a3cd96c1f
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_BecomeVisible.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_DeleteHidden.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_DeleteHidden.png
new file mode 100644
index 000000000000..73daae20460e
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_DeleteHidden.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_DeleteVisible.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_DeleteVisible.png
new file mode 100644
index 000000000000..ab9a7151246f
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_DeleteVisible.png differ
diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_InitiallyHidden.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_InitiallyHidden.png
new file mode 100644
index 000000000000..a2552743e963
Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/SwipeOpen_InitiallyHidden.png differ
diff --git a/src/Core/src/Handlers/SwipeItemMenuItem/SwipeItemMenuItemHandler.Android.cs b/src/Core/src/Handlers/SwipeItemMenuItem/SwipeItemMenuItemHandler.Android.cs
index eabc4383037b..c7192b51d799 100644
--- a/src/Core/src/Handlers/SwipeItemMenuItem/SwipeItemMenuItemHandler.Android.cs
+++ b/src/Core/src/Handlers/SwipeItemMenuItem/SwipeItemMenuItemHandler.Android.cs
@@ -71,10 +71,12 @@ public static void MapBackground(ISwipeItemMenuItemHandler handler, ISwipeItemMe
public static void MapVisibility(ISwipeItemMenuItemHandler handler, ISwipeItemMenuItem view)
{
+ // Set visibility before UpdateIsVisibleSwipeItem so LayoutSwipeItems
+ // reads the correct visibility when recalculating item positions.
+ handler.PlatformView.Visibility = view.Visibility.ToPlatformVisibility();
+
var swipeView = handler.PlatformView.Parent.GetParentOfType();
swipeView?.UpdateIsVisibleSwipeItem(view);
-
- handler.PlatformView.Visibility = view.Visibility.ToPlatformVisibility();
}
protected override AView CreatePlatformElement()
diff --git a/src/Core/src/Handlers/SwipeItemMenuItem/SwipeItemMenuItemHandler.iOS.cs b/src/Core/src/Handlers/SwipeItemMenuItem/SwipeItemMenuItemHandler.iOS.cs
index 647662e5d91b..0eb9faafd22e 100644
--- a/src/Core/src/Handlers/SwipeItemMenuItem/SwipeItemMenuItemHandler.iOS.cs
+++ b/src/Core/src/Handlers/SwipeItemMenuItem/SwipeItemMenuItemHandler.iOS.cs
@@ -88,9 +88,11 @@ public static void MapVisibility(ISwipeItemMenuItemHandler handler, ISwipeItemMe
{
var swipeView = handler.PlatformView.GetParentOfType();
- swipeView?.UpdateIsVisibleSwipeItem(view);
-
+ // Update the native view's Hidden state BEFORE calling UpdateIsVisibleSwipeItem,
+ // so LayoutSwipeItems can use the correct Hidden state when repositioning items.
handler.PlatformView.UpdateVisibility(view.Visibility);
+
+ swipeView?.UpdateIsVisibleSwipeItem(view);
}
partial class SwipeItemMenuItemImageSourcePartSetter
diff --git a/src/Core/src/Platform/Android/MauiSwipeView.cs b/src/Core/src/Platform/Android/MauiSwipeView.cs
index 56137f3cef30..f82f73f99631 100644
--- a/src/Core/src/Platform/Android/MauiSwipeView.cs
+++ b/src/Core/src/Platform/Android/MauiSwipeView.cs
@@ -610,6 +610,11 @@ void LayoutSwipeItems(List childs)
foreach (var child in childs)
{
+ if (i >= items.Count)
+ {
+ break;
+ }
+
if (child.Visibility == ViewStates.Visible)
{
var item = items[i];
@@ -655,9 +660,10 @@ void LayoutSwipeItems(List childs)
child.Layout(l, t, r, b);
- i++;
previousWidth += swipeItemWidth;
}
+
+ i++;
}
}
diff --git a/src/Core/src/Platform/iOS/MauiSwipeView.cs b/src/Core/src/Platform/iOS/MauiSwipeView.cs
index b7f316953625..01e87f888ab5 100644
--- a/src/Core/src/Platform/iOS/MauiSwipeView.cs
+++ b/src/Core/src/Platform/iOS/MauiSwipeView.cs
@@ -299,7 +299,7 @@ void UpdateSwipeItems()
double swipeItemsWidth;
if (_swipeDirection == SwipeDirection.Left || _swipeDirection == SwipeDirection.Right)
- swipeItemsWidth = items.Count * SwipeViewExtensions.SwipeItemWidth;
+ swipeItemsWidth = items.Count(GetIsVisible) * SwipeViewExtensions.SwipeItemWidth;
else
swipeItemsWidth = _contentView.Frame.Width;
@@ -312,6 +312,7 @@ void UpdateSwipeItems()
foreach (var item in items)
{
UIView swipeItem = item.ToPlatform(Element.Handler.MauiContext);
+ swipeItem.Hidden = !GetIsVisible(item);
_actionView.AddSubview(swipeItem);
_swipeItems.Add(item, swipeItem);
}
@@ -342,6 +343,11 @@ void LayoutSwipeItems(List childs)
foreach (var child in childs)
{
+ if (i >= items.Count)
+ {
+ break;
+ }
+
if (!child.Hidden)
{
var item = items[i];
@@ -371,10 +377,10 @@ void LayoutSwipeItems(List childs)
UpdateSwipeItemInsets(button);
}
- i++;
previousWidth += swipeItemWidth;
}
+ i++;
_swipeItemsRect.Add(child.Frame);
}
}
@@ -626,12 +632,12 @@ void SetFrame()
{
case SwipeDirection.Left:
_contentView.Frame = new CGRect(_originalBounds.X + offset, _originalBounds.Y, _originalBounds.Width, _originalBounds.Height);
- actionSize = Element.RightItems.Count * SwipeViewExtensions.SwipeItemWidth;
+ actionSize = Element.RightItems.Count(GetIsVisible) * SwipeViewExtensions.SwipeItemWidth;
_actionView.Frame = new CGRect(actionSize + offset, actionBounds.Y, actionBounds.Width, actionBounds.Height);
break;
case SwipeDirection.Right:
_contentView.Frame = new CGRect(_originalBounds.X + offset, _originalBounds.Y, _originalBounds.Width, _originalBounds.Height);
- actionSize = Element.LeftItems.Count * SwipeViewExtensions.SwipeItemWidth;
+ actionSize = Element.LeftItems.Count(GetIsVisible) * SwipeViewExtensions.SwipeItemWidth;
_actionView.Frame = new CGRect(-actionSize + offset, actionBounds.Y, actionBounds.Width, actionBounds.Height);
break;
case SwipeDirection.Up:
@@ -841,12 +847,12 @@ void SwipeToThreshold(bool animated = true)
{
case SwipeDirection.Left:
_contentView.Frame = new CGRect(_originalBounds.X - swipeThreshold, _originalBounds.Y, _originalBounds.Width, _originalBounds.Height);
- actionSize = Element.RightItems.Count * SwipeViewExtensions.SwipeItemWidth;
+ actionSize = Element.RightItems.Count(GetIsVisible) * SwipeViewExtensions.SwipeItemWidth;
_actionView.Frame = new CGRect(actionSize - swipeThreshold, actionBounds.Y, actionBounds.Width, actionBounds.Height);
break;
case SwipeDirection.Right:
_contentView.Frame = new CGRect(_originalBounds.X + swipeThreshold, _originalBounds.Y, _originalBounds.Width, _originalBounds.Height);
- actionSize = Element.LeftItems.Count * SwipeViewExtensions.SwipeItemWidth;
+ actionSize = Element.LeftItems.Count(GetIsVisible) * SwipeViewExtensions.SwipeItemWidth;
_actionView.Frame = new CGRect(-actionSize + swipeThreshold, actionBounds.Y, actionBounds.Width, actionBounds.Height);
break;
case SwipeDirection.Up: