How to lock orientation at runtime on iOS 16 with .NET MAUI and Xamarin.Forms

With iOS 16, Apple made some old APIs non-functional. This includes also the established way of locking the orientation. In this post, I am going to show you how you can lock orientation on iOS 16 while the app is running with both .NET MAUI and Xamarin.Forms.
ios16_orientation_lock_title_bing_create

The old way

Before iOS 16, it was pretty easy to lock a Page into a certain orientation. It was basically just one line of code (if you don’t count the DependencyService boilerplate code in):

UIDevice.CurrentDevice.SetValueForKey(new NSNumber((int)UIInterfaceOrientation.Portrait), new NSString("orientation"));

By calling this method whenever the size of a page was allocated, we were able to lock the orientation at runtime with Xamarin.Forms. With iOS 16, this does no longer work – even on native iOS applications.

The new way

To understand the why of the new way, you have to understand the SceneDelegate architecture Apple introduced with iOS 13. Before continuing, you should read this blog post by Donny Wals, which explains it very detailed: Understanding the iOS 13 Scene Delegate – Donny Wals.

Now that we know that the SceneDelegate is, we can move on with our implementation.

Page implementation

Both Xamarin.Forms and .NET MAUI implement the SceneDelegate architecture. That’s why we can update our code similarly to what native iOS implementations look like:

var rootWindowScene = (UIApplication.SharedApplication.ConnectedScenes.ToArray()?.FirstOrDefault()) as UIWindowScene;

if (rootWindowScene == null)
    return;

rootWindowScene.RequestGeometryUpdate(new UIWindowSceneGeometryPreferencesIOS(UIInterfaceOrientationMask.Portrait),
error =>
{
    Debug.WriteLine("Error while attempting to lock orientation: {Error}", error.LocalizedDescription);
});

On top, we have to tell the underlying ViewControllers to update their orientation as well:

var rootViewController = UIApplication.SharedApplication.KeyWindow?.RootViewController;

if (rootViewController == null)
    return;

rootViewController.SetNeedsUpdateOfSupportedInterfaceOrientations();
rootViewController.NavigationController?.SetNeedsUpdateOfSupportedInterfaceOrientations();

The ViewController can be informed via the SetNeedsUpdateOfSupportedInterfaceOrientations method that it needs to redraw its view. If we put this all together, we can have a reusable implementation for our DeviceOrientationService implementation:

private void SetOrientation(UIInterfaceOrientationMask uiInterfaceOrientationMask)
{
    var rootWindowScene = (UIApplication.SharedApplication.ConnectedScenes.ToArray()?.FirstOrDefault()) as UIWindowScene;
    
    if (rootWindowScene == null)
        return;
    
    var rootViewController = UIApplication.SharedApplication.KeyWindow?.RootViewController;

    if (rootViewController == null)
        return;
    
    rootWindowScene.RequestGeometryUpdate(new UIWindowSceneGeometryPreferencesIOS(uiInterfaceOrientationMask),
    error =>
    {
        Debug.WriteLine("Error while attempting to lock orientation: {Error}", error.LocalizedDescription);
    });
    
    rootViewController.SetNeedsUpdateOfSupportedInterfaceOrientations();
    rootViewController.NavigationController?.SetNeedsUpdateOfSupportedInterfaceOrientations();
}

To keep our existing code for older iOS versions working as well. We now just check if we are on iOS 16 and call our new method, below we still can use our traditional way:

public void LockPortrait()
{
    if (UIDevice.CurrentDevice.CheckSystemVersion(16, 0))
    {
        _applicationDelegate.CurrentLockedOrientation = UIInterfaceOrientationMask.Portrait;
    
        SetOrientation(UIInterfaceOrientationMask.Portrait);
    }
    else
    {
        UIDevice.CurrentDevice.SetValueForKey(new NSNumber((int)UIInterfaceOrientation.Portrait), new NSString("orientation"));
    }
}

This will, however, do nothing without the last, very important step. You may have noticed the CurrentLockedOrientation property on the application delegate member above.

Every time the application has to decide whether to rotate or not, the application:supportedInterfaceOrientationsForWindow: gets called to ask for the supported orientations. Only if the application and the ViewController agree on the supported orientations, the action will be executed.

Extending our AppDelegate

Just as on native iOS, we need to implement the method above in our AppDelegate. Xamarin and .NET MAUI do this via the Export attribute, which tells the compiler to override the eventually existing native implementation.

For my solution, I created a derived version of the FormsApplicationDelegate / MauiUIApplicationDelegate classes, passing the current desired UIInterfaceOrientationMask value to the CurrentLockedOrientation property. Finally, I implement the GetSupportedInterfaceOrientationsForWindow method and just return the value of the CurrentLockedOrientation property:

public abstract class AppDelegateEx : MauiUIApplicationDelegate
{
    public virtual UIInterfaceOrientationMask CurrentLockedOrientation { get; set; }

    //according to the Apple docs, Application and ViewController have to agree on the supported orientation, this forces it
    //https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623107-application?language=objc
    [Foundation.Export("application:supportedInterfaceOrientationsForWindow:")]
    public virtual UIInterfaceOrientationMask GetSupportedInterfaceOrientationsForWindow(UIApplication application, UIWindow forWindow)
        => this.CurrentLockedOrientation;
}

Now we just make the AppDelegate derive from AppDelegateEx (or whatever you call it) to finish the implementation for the orientation lock. Finally, locking the orientation works also on iOS 16.

Samples

I created two samples – one for Xamarin.Forms and one for .NET MAUI. The sample work similar on both platforms, and I wrote the code in a reusable way. You can find the samples in the corresponding GitHub repo.

Conclusion

It took me some time to figure out why the traditional way of locking the orientation doesn’t work any longer. After some research and some trial-and-error coding, I was able to come up with a clean and easy-to use solution, which is also reusable. I also learned some new things, like how MAUI implements Scenes and ViewControllers and got a better understanding of the iOS application structure and lifecycle on newer OS versions.

As always, I hope this post will be helpful for some of you as well.

Until the next post, happy coding!


Helpful links:


Title Image created via Bing Create with AI

Comments 4
  1. Great article – but unfortunately this does only work for iPhones and not for iPads.
    If you test the MAUI app from the GitHub repo with an iPad simulator or a real iPad device, locking the screen orientation is not possible (for iPhones it is working fine).

    ISSUE:
    rootWindowScene.RequestGeometryUpdate(new UIWindowSceneGeometryPreferencesIOS(uiInterfaceOrientationMask),
    error =>
    {
    _logger.LogError(“Error while attempting to lock orientation: {Error}”, error.LocalizedDescription);
    });

    error.LocalizedDescription: The current windowing mode does not allow for programmatic changes to interface orientation.

    Is there a workaround for tablets?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Prev
Deploy MAUI apps with Rider on your iOS device after these Xcode errors
JetBrains Rider error on deploy .NET MAUI iOS

Deploy MAUI apps with Rider on your iOS device after these Xcode errors

Next
#CASBAN6: Add a Swagger (OpenAPI) page to Azure Functions
A screenshot of the #CASBAN6 OpenAPI Swagger page

#CASBAN6: Add a Swagger (OpenAPI) page to Azure Functions

You May Also Like

This website uses cookies. By continuing to use this site, you accept the use of cookies.  Learn more