diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySwitchOffColorAfterReopeningApp.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySwitchOffColorAfterReopeningApp.png new file mode 100644 index 000000000000..b09bc37b4006 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/VerifySwitchOffColorAfterReopeningApp.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue29768.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue29768.cs new file mode 100644 index 000000000000..4bbab466f3b3 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue29768.cs @@ -0,0 +1,49 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 29768, "Switch OffColor not displayed after minimizing and reopening the app", PlatformAffected.iOS)] +public class Issue29768 : ContentPage +{ + public Issue29768() + { + + var defaultOffSwitch = new Switch + { + OffColor = Colors.Red, + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center + }; + + var switchControl = new Switch + { + IsToggled = true, + OffColor = Colors.Red, + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center + }; + + var button = new Button + { + Text = "Toggle Switch 2", + AutomationId = "toggleButton", + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center + }; + + button.Clicked += (sender, e) => + { + switchControl.IsToggled = !switchControl.IsToggled; + }; + + var verticalStackLayout = new VerticalStackLayout() + { + Spacing = 20, + Padding = new Thickness(20), + }; + + verticalStackLayout.Add(defaultOffSwitch); + verticalStackLayout.Add(switchControl); + verticalStackLayout.Add(button); + + Content = verticalStackLayout; + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29768.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29768.cs new file mode 100644 index 000000000000..721b3646ac96 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue29768.cs @@ -0,0 +1,28 @@ +#if TEST_FAILS_ON_WINDOWS && TEST_FAILS_ON_CATALYST //The test fails on Windows and MacCatalyst because the BackgroundApp and ForegroundApp method, which is only supported on mobile platforms iOS and Android. +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue29768 : _IssuesUITest +{ + public override string Issue => "Switch OffColor not displayed after minimizing and reopening the app"; + + public Issue29768(TestDevice device) + : base(device) + { } + + [Test] + [Category(UITestCategories.Switch)] + public void VerifySwitchOffColorAfterReopeningApp() + { + App.WaitForElement("toggleButton"); + App.Tap("toggleButton"); + App.BackgroundApp(); + App.ForegroundApp(); + + VerifyScreenshot(); + } +} +#endif \ No newline at end of file diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifySwitchOffColorAfterReopeningApp.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifySwitchOffColorAfterReopeningApp.png new file mode 100644 index 000000000000..e6a23a3f2b8b Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/VerifySwitchOffColorAfterReopeningApp.png differ diff --git a/src/Core/src/Handlers/Switch/SwitchHandler.iOS.cs b/src/Core/src/Handlers/Switch/SwitchHandler.iOS.cs index 26f043ffe788..65c2294a2471 100644 --- a/src/Core/src/Handlers/Switch/SwitchHandler.iOS.cs +++ b/src/Core/src/Handlers/Switch/SwitchHandler.iOS.cs @@ -1,5 +1,8 @@ using System; using Microsoft.Maui.Graphics; +using System.Threading.Tasks; +using CoreFoundation; +using Foundation; using ObjCRuntime; using UIKit; using RectangleF = CoreGraphics.CGRect; @@ -60,21 +63,81 @@ class SwitchProxy ISwitch? VirtualView => _virtualView is not null && _virtualView.TryGetTarget(out var v) ? v : null; + WeakReference? _platformView; + + UISwitch? PlatformView => _platformView is not null && _platformView.TryGetTarget(out var p) ? p : null; + + NSObject? _willEnterForegroundObserver; + NSObject? _windowDidBecomeKeyObserver; + public void Connect(ISwitch virtualView, UISwitch platformView) { _virtualView = new(virtualView); + _platformView = new(platformView); platformView.ValueChanged += OnControlValueChanged; + +#if MACCATALYST + _windowDidBecomeKeyObserver = NSNotificationCenter.DefaultCenter.AddObserver( + new NSString("NSWindowDidBecomeKeyNotification"), _ => + { + if (PlatformView is not null) + { + UpdateTrackOffColor(PlatformView); + } + }); +#elif IOS + _willEnterForegroundObserver = NSNotificationCenter.DefaultCenter.AddObserver( + UIApplication.WillEnterForegroundNotification, _ => + { + if (PlatformView is not null) + { + UpdateTrackOffColor(PlatformView); + } + }); +#endif + } + + // Ensures the Switch track "OFF" color is updated correctly after system-level UI resets. + // This is necessary because UIKit may re-apply default styles to internal views during certain lifecycle events, + // especially when the app enters the background and returns to the foreground. + void UpdateTrackOffColor(UISwitch platformView) + { + DispatchQueue.MainQueue.DispatchAsync(async () => + { + if (!platformView.On) + { + await Task.Delay(10); // Small delay, necessary to allow UIKit to complete its internal layout and styling processes before re-applying the custom color + + if (VirtualView is ISwitch view && view.TrackColor is not null) + { + platformView.UpdateTrackColor(view); + } + } + }); } public void Disconnect(UISwitch platformView) { platformView.ValueChanged -= OnControlValueChanged; + + if (_willEnterForegroundObserver is not null) + { + NSNotificationCenter.DefaultCenter.RemoveObserver(_willEnterForegroundObserver); + _willEnterForegroundObserver = null; + } + if (_windowDidBecomeKeyObserver is not null) + { + NSNotificationCenter.DefaultCenter.RemoveObserver(_windowDidBecomeKeyObserver); + _windowDidBecomeKeyObserver = null; + } } void OnControlValueChanged(object? sender, EventArgs e) { if (VirtualView is ISwitch virtualView && sender is UISwitch platformView && virtualView.IsOn != platformView.On) + { virtualView.IsOn = platformView.On; + } } } }