Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue28968.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 28968, "[iOS] ActivityIndicator IsRunning ignores IsVisible when set to true", PlatformAffected.iOS)]
public class Issue28968 : ContentPage
{
public Issue28968()
{
var activityIndicator = new ActivityIndicator
{
IsVisible = false,
AutomationId = "MauiActivityIndicator"
};

var statusLabel = new Label
{
Text = "Waiting",
AutomationId = "StatusLabel"
};

var setRunningButton = new Button
{
Text = "Set IsRunning = true",
AutomationId = "SetRunningButton",
Command = new Command(() =>
{
activityIndicator.IsRunning = true;

// Check the native platform hidden state after a delay to allow
// Draw/LayoutSubviews to execute on iOS
Dispatcher.DispatchDelayed(TimeSpan.FromMilliseconds(500), () =>
{
bool nativeHidden = IsNativeViewHidden(activityIndicator);
statusLabel.Text = nativeHidden ? "HIDDEN" : "VISIBLE";
});
})
};

Content = new VerticalStackLayout
{
VerticalOptions = LayoutOptions.Center,
HorizontalOptions = LayoutOptions.Center,
Children =
{
activityIndicator,
setRunningButton,
statusLabel
}
};
}

static bool IsNativeViewHidden(ActivityIndicator indicator)
{
var handler = indicator.Handler;
if (handler?.PlatformView is null)
return true;

#if IOS || MACCATALYST
if (handler.PlatformView is UIKit.UIView nativeView)
return nativeView.Hidden;
#elif ANDROID
if (handler.PlatformView is Android.Views.View nativeView)
return nativeView.Visibility != Android.Views.ViewStates.Visible;
#elif WINDOWS
if (handler.PlatformView is Microsoft.UI.Xaml.UIElement nativeView)
return nativeView.Visibility != Microsoft.UI.Xaml.Visibility.Visible;
#endif
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Tests.Issues;

public class Issue28968 : _IssuesUITest
{
public Issue28968(TestDevice device) : base(device)
{
}

public override string Issue => "[iOS] ActivityIndicator IsRunning ignores IsVisible when set to true";

[Test]
[Category(UITestCategories.ActivityIndicator)]
public void ActivityIndicatorIsRunningDoesNotOverrideIsVisible()
{
App.WaitForElement("SetRunningButton");
App.Tap("SetRunningButton");

// Wait for the status label to update after the delayed check
var statusText = App.WaitForElement("StatusLabel").GetText();

// Retry a few times since the dispatcher delay in the host app
// means the label won't update immediately
int retries = 10;
while (statusText == "Waiting" && retries-- > 0)
{
Thread.Sleep(200);
statusText = App.WaitForElement("StatusLabel").GetText();
}

Assert.That(statusText, Is.EqualTo("HIDDEN"),
"ActivityIndicator should remain hidden when IsVisible=false, even after IsRunning is set to true");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ public partial class ActivityIndicatorHandler : IActivityIndicatorHandler
{
[nameof(IActivityIndicator.Color)] = MapColor,
[nameof(IActivityIndicator.IsRunning)] = MapIsRunning,
#if __ANDROID__
// Android does not have the concept of IsRunning, so we are leveraging the Visibility
#if __ANDROID__ || __IOS__ || MACCATALYST
// Android/iOS do not respect both properties independently, so we handle Visibility explicitly
[nameof(IActivityIndicator.Visibility)] = MapIsRunning,
#endif
#if WINDOWS
Expand Down Expand Up @@ -91,4 +91,4 @@ public ActivityIndicatorHandler(IPropertyMapper? mapper, CommandMapper? commandM
public static partial void MapBackground(IActivityIndicatorHandler handler, IActivityIndicator activityIndicator);
#endif
}
}
}
15 changes: 12 additions & 3 deletions src/Core/src/Platform/iOS/ActivityIndicatorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@ public static class ActivityIndicatorExtensions
{
public static void UpdateIsRunning(this UIActivityIndicatorView activityIndicatorView, IActivityIndicator activityIndicator)
{
if (activityIndicator.IsRunning)
// Only show and animate if both IsRunning AND Visibility == Visible
if (activityIndicator.IsRunning && activityIndicator.Visibility == Visibility.Visible)
{
activityIndicatorView.Hidden = false;
activityIndicatorView.StartAnimating();
}
else
activityIndicatorView.StopAnimating();
{
if (activityIndicatorView.IsAnimating)
activityIndicatorView.StopAnimating();

activityIndicatorView.Hidden = activityIndicator.Visibility != Visibility.Visible;
}
}

public static void UpdateColor(this UIActivityIndicatorView activityIndicatorView, IActivityIndicator activityIndicator)
=> activityIndicatorView.Color = activityIndicator.Color?.ToPlatform();
}
}
}
11 changes: 7 additions & 4 deletions src/Core/src/Platform/iOS/MauiActivityIndicator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ public class MauiActivityIndicator : UIActivityIndicatorView, IUIViewLifeCycleEv
{
readonly WeakReference<IActivityIndicator>? _virtualView;

bool IsRunning => _virtualView is not null && _virtualView.TryGetTarget(out var a) ? a.IsRunning : false;
bool IsRunningAndVisible => _virtualView is not null &&
_virtualView.TryGetTarget(out var a) &&
a.IsRunning &&
a.Visibility == Visibility.Visible;

public MauiActivityIndicator(CGRect rect, IActivityIndicator? virtualView) : base(rect)
{
Expand All @@ -22,7 +25,7 @@ public override void Draw(CGRect rect)
{
base.Draw(rect);

if (IsRunning)
if (IsRunningAndVisible)
StartAnimating();
else
StopAnimating();
Expand All @@ -32,7 +35,7 @@ public override void LayoutSubviews()
{
base.LayoutSubviews();

if (IsRunning)
if (IsRunningAndVisible)
StartAnimating();
else
StopAnimating();
Expand All @@ -57,4 +60,4 @@ public override void MovedToWindow()
_movedToWindow?.Invoke(this, EventArgs.Empty);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,85 @@ namespace Microsoft.Maui.DeviceTests
[Category(TestCategory.ActivityIndicator)]
public partial class ActivityIndicatorHandlerTests : CoreHandlerTestBase<ActivityIndicatorHandler, ActivityIndicatorStub>
{
#if !WINDOWS // On Windows, the platform control will return IsActive as true even when the control is not visible.
[Theory(DisplayName = "IsRunning Should Respect Visibility At Init")]
[InlineData(true, true)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(false, false)]
public async Task IsRunningShouldRespectVisibilityAtInit(bool isRunning, bool isVisible)
{
var activityIndicator = new ActivityIndicatorStub
{
IsRunning = isRunning,
Visibility = isVisible ? Visibility.Visible : Visibility.Hidden
};

bool isAnimating = false;

await InvokeOnMainThreadAsync(() =>
{
var handler = CreateHandler(activityIndicator);
isAnimating = GetNativeIsRunning(handler);
});

if (isVisible && isRunning)
Assert.True(isAnimating);
else
Assert.False(isAnimating);
}

[Fact(DisplayName = "Setting IsRunning After Init Should Respect Hidden Visibility")]
public async Task SettingIsRunningAfterInitShouldRespectHiddenVisibility()
{
var activityIndicator = new ActivityIndicatorStub
{
IsRunning = false,
Visibility = Visibility.Hidden
};

bool isAnimating = false;

await InvokeOnMainThreadAsync(() =>
{
var handler = CreateHandler(activityIndicator);

// Simulate runtime: set IsRunning=true while Visibility=Hidden
activityIndicator.IsRunning = true;
handler.UpdateValue(nameof(IActivityIndicator.IsRunning));

isAnimating = GetNativeIsRunning(handler);
});

Assert.False(isAnimating, "ActivityIndicator should not animate when Visibility is Hidden");
}

[Fact(DisplayName = "Setting IsRunning After Init Should Respect Collapsed Visibility")]
public async Task SettingIsRunningAfterInitShouldRespectCollapsedVisibility()
{
var activityIndicator = new ActivityIndicatorStub
{
IsRunning = false,
Visibility = Visibility.Collapsed
};

bool isAnimating = false;

await InvokeOnMainThreadAsync(() =>
{
var handler = CreateHandler(activityIndicator);

// Simulate runtime: set IsRunning=true while Visibility=Collapsed
activityIndicator.IsRunning = true;
handler.UpdateValue(nameof(IActivityIndicator.IsRunning));

isAnimating = GetNativeIsRunning(handler);
});

Assert.False(isAnimating, "ActivityIndicator should not animate when Visibility is Collapsed");
}
#endif

[Theory(DisplayName = "IsRunning Initializes Correctly")]
[InlineData(true)]
[InlineData(false)]
Expand Down
Loading