Forcing specific MAUI view to Landscape Orientation using MultiTargeting Feature working for Android but not iOS
Asked Answered
T

4

9

I need a specific MAUI page to be in Landscape only orientation. I found this tutorial about forcing device orientation and I am using the multi-targeting feature of MAUI to implement the device specific code needed to force this orientation. The tutorial says that they didn't test the iOS version. I have the tutorial working for Android (allows programmatic forcing of Landscape orientation for a single page through a singleton service) but not for iOS.

using System;
namespace ScoreKeepersBoard.DeviceServices;

public partial class DeviceOrientationService : IDeviceOrientationService
{
    public partial void SetDeviceOrientation(DisplayOrientation displayOrientation);
}

Here is where I inject my device orientation service into my view model and set the orientation to landscape:

public partial class NewGameViewModel : ObservableObject
{
    IGameTypeDataAccess gameTypeDataAccess;
    ITeamDataAccess teamDataAccess;
    IDeviceOrientationService deviceOrientationService;

    [ObservableProperty]
    IList<GameType> gameTypes = new List<GameType>();

    [ObservableProperty]
    private GameType selectedGameType;

    [ObservableProperty]
    private string gameTypeSelectionError;

    [ObservableProperty]
    private ObservableCollection<Team> teamOneTeams = new ObservableCollection<Team>();

    [ObservableProperty]
    private Team teamOneSelection;

    [ObservableProperty]
    private string teamOneSelectionError;

    [ObservableProperty]
    private ObservableCollection<Team> teamTwoTeams = new ObservableCollection<Team>();

    [ObservableProperty]
    private Team teamTwoSelection;

    [ObservableProperty]
    private string teamTwoSelectionError;

    private ObservableCollection<Team> allTeams = new ObservableCollection<Team>();

    private bool react = true;

    public NewGameViewModel(IGameTypeDataAccess iGameTypeDataAccess, ITeamDataAccess iTeamDataAccess, IDeviceOrientationService iDeviceOrientationService)
    {
        gameTypeDataAccess = iGameTypeDataAccess;
        teamDataAccess = iTeamDataAccess;
        deviceOrientationService = iDeviceOrientationService;

        

        deviceOrientationService.SetDeviceOrientation(DisplayOrientation.Landscape);
    }
}

And here is my multi targeted code in the /Platforms/Android folder:

using System;
using Android.Content.PM;

namespace ScoreKeepersBoard.DeviceServices;

public partial class DeviceOrientationService
{

    private static readonly IReadOnlyDictionary<DisplayOrientation, ScreenOrientation> _androidDisplayOrientationMap =
        new Dictionary<DisplayOrientation, ScreenOrientation>
        {
            [DisplayOrientation.Landscape] = ScreenOrientation.Landscape,
            [DisplayOrientation.Portrait] = ScreenOrientation.Portrait,
        };

    public partial void SetDeviceOrientation(DisplayOrientation displayOrientation)
    {
        var currentActivity = ActivityStateManager.Default.GetCurrentActivity();
        if(currentActivity is not null)
        {
            if(_androidDisplayOrientationMap.TryGetValue(displayOrientation, out ScreenOrientation screenOrientation))
            {
                currentActivity.RequestedOrientation = screenOrientation;
            }
        }
    }
}

I have similar setup for multi-targeted to iOS in /Platforms/iOS. UPDATE: I Edited my code according to the answer from Dongzhi Wang-MSFT

using System;
using Foundation;
using UIKit;

namespace ScoreKeepersBoard.DeviceServices;

public partial class DeviceOrientationService
{

    private static readonly IReadOnlyDictionary<DisplayOrientation, UIInterfaceOrientation> _iosDisplayOrientationMap =
        new Dictionary<DisplayOrientation, UIInterfaceOrientation>
        {
            [DisplayOrientation.Landscape] = UIInterfaceOrientation.LandscapeLeft,
            [DisplayOrientation.Portrait] = UIInterfaceOrientation.Portrait,
        };

    public partial void SetDeviceOrientation(DisplayOrientation displayOrientation)
    {

        if (UIDevice.CurrentDevice.CheckSystemVersion(16, 0))
        {

            var scene = (UIApplication.SharedApplication.ConnectedScenes.ToArray()[0] as UIWindowScene);
            if (scene != null)
            {
                var uiAppplication = UIApplication.SharedApplication;
                var test = UIApplication.SharedApplication.KeyWindow?.RootViewController;
                if (test != null)
                {
                    test.SetNeedsUpdateOfSupportedInterfaceOrientations();
                    scene.RequestGeometryUpdate(
                        new UIWindowSceneGeometryPreferencesIOS(UIInterfaceOrientationMask.Portrait), error => { });
                }
            }
        }
        else
        {
            UIDevice.CurrentDevice.SetValueForKey(new NSNumber((int)UIInterfaceOrientation.Portrait), new NSString("orientation"));
        }


    }
}

This forces the orientation to Portrait but when I switch from Portrait to Landscape the layout first switches to Landscape and then gets forced into Portrait as shown in the GIF image below.

How can I KEEP it in Portrait as the user changes the orientation?

enter image description here

UPDATE: I updated my .NET MAUI and the update required me to use XCODE 14.2 and now my virtual emulators are all running iOS 16.2 and now the iOS version of the code doesn't work at all and doesn't lock the screen into any orientation. I get this warning now in the iOS platform specific code:

enter image description here

enter image description here

It looks like for iOS version 16.2 this solution doesn't work anymore!

Tinatinamou answered 17/12, 2022 at 22:52 Comment(4)
If you put a breakpoint on if (_iosDisplayOrientationMap.TryGetValue..., then step through the code, does SetStatusBarOrientation line get executed?Paniculate
Yes, I have tried that and SetStatusBarOrientation does get executed.Tinatinamou
I wrote up a tutorial on how to do the cross-platform API and device specific implementations of this on my website CodeShadowHand: codeshadowhand.com/… The iOS platform specific code still breaks for iOS version 16.2.Tinatinamou
I haven't tested, but what's needed should be similar to what is done now in place of deprecated shouldAutorotate: "update supportedInterfaceOrientations and then call setNeedsUpdateOfSupportedInterfaceOrientations()". In Apple docs, those are calls on UIViewController; IIRC, the MAUI equivalent is a custom PageRenderer (or custom handler).Paniculate
B
4

Ok, in the end I did this in my application.

I wanted to force the orientation only for one specific page, not for all, this is why I changed all the procedures as follows:

DeviceOrientationService.Android.cs

public partial class DeviceOrientationService
{
    private static readonly IReadOnlyDictionary<DisplayOrientation, ScreenOrientation> _androidDisplayOrientationMap =
        new Dictionary<DisplayOrientation, ScreenOrientation>
        {
            [DisplayOrientation.Landscape] = ScreenOrientation.Landscape,
            [DisplayOrientation.Portrait] = ScreenOrientation.Portrait,
        };

    public partial async Task SetDeviceOrientation(DisplayOrientation displayOrientation)
    {
        var currentActivity = ActivityStateManager.Default.GetCurrentActivity();
        if (currentActivity is not null)
        {
            if (_androidDisplayOrientationMap.TryGetValue(displayOrientation, out ScreenOrientation screenOrientation))
                currentActivity.RequestedOrientation = screenOrientation;
        }
        await Task.Delay(100);
    }
}

DeviceOrientationService.iOS.cs

public partial class DeviceOrientationService
{
    private static readonly IReadOnlyDictionary<DisplayOrientation, UIInterfaceOrientation> _iosDisplayOrientationMap =
        new Dictionary<DisplayOrientation, UIInterfaceOrientation>
        {
            [DisplayOrientation.Landscape] = UIInterfaceOrientation.LandscapeLeft,
            [DisplayOrientation.Portrait] = UIInterfaceOrientation.Portrait,
        };

    public partial async Task SetDeviceOrientation(DisplayOrientation displayOrientation)
    {
        if (_iosDisplayOrientationMap.TryGetValue(displayOrientation, out var iosOrientation))
        {
            if (UIDevice.CurrentDevice.CheckSystemVersion(16, 0))
            {
                var scene = (UIApplication.SharedApplication.ConnectedScenes.ToArray()[0] as UIWindowScene);
                if (scene != null)
                {
                    var uiAppplication = UIApplication.SharedApplication;
                    var test = UIApplication.SharedApplication.KeyWindow?.RootViewController;
                    if (test != null)
                    {
                        UIInterfaceOrientationMask NewOrientation;
                        if (iosOrientation == UIInterfaceOrientation.Portrait)
                        {
                            NewOrientation = UIInterfaceOrientationMask.Portrait;
                        }
                        else
                        {
                            NewOrientation = UIInterfaceOrientationMask.LandscapeLeft;
                        }
                        scene.Title = "PerformOrientation";
                        scene.RequestGeometryUpdate(
                            new UIWindowSceneGeometryPreferencesIOS(NewOrientation), error => { System.Diagnostics.Debug.WriteLine(error.ToString()); });
                        test.SetNeedsUpdateOfSupportedInterfaceOrientations();
                        test.NavigationController?.SetNeedsUpdateOfSupportedInterfaceOrientations();
                        await Task.Delay(1000); //Gives the time to apply the view rotation
                        scene.Title = "";
                    }
                }
            }
            else
            {
                UIDevice.CurrentDevice.SetValueForKey(new NSNumber((int)iosOrientation), new NSString("orientation"));
            }
        }
    }
}

Very important, in the AppDelegate.cs file (/Platforms/iOS) you should add:

[Export("application:supportedInterfaceOrientationsForWindow:")]
public UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow)
{
    if (forWindow.WindowScene != null && forWindow.WindowScene.Title == "PerformOrientation")
    {
        return UIInterfaceOrientationMask.All;
    }
    else
    {
        return application.SupportedInterfaceOrientationsForWindow(forWindow);
    }
}

and everything works great! Also, after you force the first time the orientation, if the device gets rotated to an orientation which is not in the info.plist, the orientation will not take place.

Benzaldehyde answered 3/3, 2023 at 11:25 Comment(0)
A
1

The existing answers didn't resolve my orientation issues on iOS, for iOS here is how I handled it.

Define Interface

public interface IDeviceOrientationService
{
    void SetDeviceOrientation(DisplayOrientation displayOrientation);
}

iOS IDeviceOrientationService implementation

public class DeviceOrientationService : IDeviceOrientationService
{
     private readonly IReadOnlyDictionary<DisplayOrientation, UIInterfaceOrientation> _iosDisplayOrientationMap =
        new Dictionary<DisplayOrientation, UIInterfaceOrientation>
        {
            [DisplayOrientation.Landscape] = UIInterfaceOrientation.LandscapeLeft,
            [DisplayOrientation.Portrait] = UIInterfaceOrientation.Portrait,
        };

    public void SetDeviceOrientation(DisplayOrientation displayOrientation)
    {
        OrientationManager.LockOrientation = true;
        if (displayOrientation == DisplayOrientation.Portrait)
        {
            OrientationManager.CurrentOrientation = UIInterfaceOrientationMask.Portrait;
        }
        else
        {
            OrientationManager.CurrentOrientation = UIInterfaceOrientationMask.Landscape;
        }
       
        UIDevice.CurrentDevice.SetValueForKey(new NSNumber((int)_iosDisplayOrientationMap[displayOrientation]), new NSString("orientation"));
    }
}

public static class OrientationManager
{
    public static bool LockOrientation { get; set; }
    public static UIInterfaceOrientationMask CurrentOrientation { get; set; } = UIInterfaceOrientationMask.All;
}

App Delegate:

    [Export("application:supportedInterfaceOrientationsForWindow:")]
    public UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow)
    {
        return OrientationManager.LockOrientation 
            ? OrientationManager.CurrentOrientation 
            : UIInterfaceOrientationMask.All;
    }

Android (same as existing answer)

public class DeviceOrientationService : IDeviceOrientationService
{
    private static readonly IReadOnlyDictionary<DisplayOrientation, ScreenOrientation> _androidDisplayOrientationMap =
        new Dictionary<DisplayOrientation, ScreenOrientation>
        {
            [DisplayOrientation.Landscape] = ScreenOrientation.Landscape,
            [DisplayOrientation.Portrait] = ScreenOrientation.Portrait,
        };

    public void SetDeviceOrientation(DisplayOrientation displayOrientation)
    {
        var currentActivity = ActivityStateManager.Default.GetCurrentActivity();
        if (currentActivity is not null)
        {
            if (_androidDisplayOrientationMap.TryGetValue(displayOrientation, out ScreenOrientation screenOrientation))
                currentActivity.RequestedOrientation = screenOrientation;
        }
    }
}
Aleutian answered 13/5 at 11:4 Comment(0)
F
0

In Maui, you can use Invoke platform code to call the iOS native API to achieve. For details, please refer to the official documentation: Invoke platform code | Microsoft.

For iOS part of the code:

if (UIDevice.CurrentDevice.CheckSystemVersion(16, 0))
            {
                var scene = (UIApplication.SharedApplication.ConnectedScenes.ToArray()[0] as UIWindowScene);
                if(scene != null)
                {
                    var test = UIApplication.SharedApplication.KeyWindow?.RootViewController;
                   if (test != null)
                    {
                        test.SetNeedsUpdateOfSupportedInterfaceOrientations();
                        scene.RequestGeometryUpdate(
                            new UIWindowSceneGeometryPreferencesIOS(UIInterfaceOrientationMask.Landscape), error => { });
                    }
                }
            }   
            else
            {
                UIDevice.CurrentDevice.SetValueForKey(new NSNumber((int)UIInterfaceOrientation.LandscapeLeft), new NSString("orientation"));
            }
Fonsie answered 19/12, 2022 at 8:1 Comment(4)
Hello, I added your code to the iOS platform code and now my iphone simulator forces the orientation into portrait. The only issue though is if you are in Portrait and switch to landscape the layout temporarily changes to Landscape and then switches back to portrait (see gif video I added to my post). How can I keep the layout in Portrait? It looks a bit funny having it switch back and forth.Tinatinamou
You try adding breakpoints in your code to see how it executes. Looks like it switched to landscape first and then forced portrait.Fonsie
I updated my .NET MAUI and the update required me to use XCODE 14.2 and now my virtual emulators are all running iOS 16.2 and now the iOS version of the code doesn't work at all and doesn't lock the screen into any orientation. I get this warning now in the iOS platform specific code: CA1416: "This call site is reachable on: 'iOS' 11.0 and later, 'maccatalyst' 11.0 and later. 'UIWindowSceneGeometryPreferencesIOS' is only supported on: 'ios' 16.0 and later, 'maccatalyst' 16.0 and later." Check out my updated post with pictures. Can you help me out?Tinatinamou
Did you have this problem after updating the version? You can report this issue on GitHub, before you can return to the previous version to continue using.Fonsie
C
0

One can install the Nuget package Plugin.Maui.DeviceOrientation which is based on the Xamarin.Forms Plugin.DeviceOrientation package by Yauheni Pakala

https://github.com/compusport/Plugin.Maui.DeviceOrientation

Choric answered 20/8 at 10:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.