Search code examples
iosdelphifiremonkeydelphi-10-seattle

Delphi/Firemonkey Change iOS screen rotation at runtime


basically all I want to achieve is when a user is in a certain part of the App to change the screen rotation as needed, I have this working for Andriod and I can not see why it shouldn't work for iOS

procedure TForm1.Button1Click(Sender: TObject);
var
  ScreenService: IFMXScreenService;
  OrientSet: TScreenOrientations;
begin
    if TPlatformServices.Current.SupportsPlatformService(IFMXScreenService, IInterface(ScreenService)) 
    then
    begin
        OrientSet := [TScreenOrientation.soLandscape];//<- Break point set here and line is executed
        ScreenService.SetScreenOrientation(OrientSet);
    end;
end;

Taken from here : How to prevent screen rotation with android development in delphi xe5 Firemonkey

The ScreenService.SetScreenOrientation is executed and does not raise a exception but the orientation is not changed, I have also set Enable custom orientation in Project>Options>Application>Orientation but this also didn't have any effect.

What is strange to me is that if it was not supported then shouldn't this

if TPlatformServices.Current.SupportsPlatformService(IFMXScreenService, IInterface(ScreenService)) 

Return false? and not even enter the begin

I added a test button to check the screen orientation after I set it to landscape only with

if TPlatformServices.Current.SupportsPlatformService(IFMXScreenService, IInterface(ScreenService)) 
then
begin
    case ScreenService.GetScreenOrientation of
        TScreenOrientation.Portrait: ShowMessage('Portrait');
        TScreenOrientation.Landscape: ShowMessage('landscape');
        TScreenOrientation.InvertedPortrait: ShowMessage('Inverted-Portrait');
        TScreenOrientation.InvertedLandscape: ShowMessage('Inverted-Landscape');
        else ShowMessage('not set');
    end;
end;

And if it was in Portrait after setting it to Landscape it still says Portrait

Update 1 : I have also tried changing

OrientSet := [TScreenOrientation.soLandscape] // <- Deprecated

to

OrientSet := [TScreenOrientation.Landscape]

but the behaviour is still the same


Solution

  • Ok this rotation had me digging deep into the iOS API to figure out how iOS manages orientation.

    Because you can not fake a rotation or force a device certain device orientation in iOS in iOS you have quite a few orientations to consider

    • Device Orientation
    • Statusbar orientation
    • UIViewcontroller orientation

    The device orientation can not be forced or altered this will always show the orientation your device is in now.

    The statusbar orientation however has always had the setStatusBarOrientation procedure you could call and spesify the orientation you wanted, but this was marked as deprecated, and thus was not working, but I didn't get a external exception when I called the function, which ment that it was still there, it just wasn't working.

    then I read this :

    The setStatusBarOrientation:animated: method is not deprecated outright. It now works only if the supportedInterfaceOrientations method of the top-most full-screen view controller returns 0

    I then decided to try this

    Application.FormFactor.Orientations := []; //the supportedInterfaceOrientations returns 0
    App := TUIApplication.Wrap(TUIApplication.OCClass.sharedApplication);
    win := TUIWindow.Wrap(App.windows.objectAtIndex(0));
    App.setStatusBarOrientation(UIInterfaceOrientationLandscapeLeft);
    

    And it works the statusbar orientation changes, but like mentioned above, this protocol is deprecated so at some stage it can/will fall away and this will no longer work.

    but this only changed the rotation of the statusbar and all alertviews etc, but the actual content within the rootviewcontroller is still in the Portrait in my case of wanting to change to landscape.

    I then tought if I would in Delphi would go to Application->Orientation-> Enable custom rotation and say landscape only then the App will only display in landscape, and it does perfectly because when the form is created and then rootViewcontroller is made the supportedInterface orientations that is returned is only landscape and the Viewcontroller goes to Landscape.

    Because when your device changes device orientation the orientation is evaluated against the Viewcontroller's supportedinterfaceorientations, if the orientation is not supported it is simply not rotated.

    But when a new rootviewcontroller is assigned the GUI has to be updated to display in that Viewcontrollers supported orientations, with that in mind, here is my fix/hack to change the orientation of your App to a orientation you want.

    TL;DR

    Uses: iOSapi.UIKit; 
    
    procedure ChangeiOSorientation(toOrientation: UIInterfaceOrientation;
    possibleOrientations: TScreenOrientations);
    var
        win : UIWindow;
        App : UIApplication;
        viewController : UIViewController;
        oucon: UIViewController;
    begin
        Application.FormFactor.Orientations := []; //Change supported orientations
        App := TUIApplication.Wrap(TUIApplication.OCClass.sharedApplication);
        win := TUIWindow.Wrap(App.windows.objectAtIndex(0)); //The first Windows is always the main Window
    
        App.setStatusBarOrientation(toOrientation);
        {After you have changed your statusbar orientation set the
        Supported orientation/orientations to whatever you need}
        Application.FormFactor.Orientations := possibleOrientations;
        viewController := TUIViewController.Wrap(TUIViewController.alloc.init);//dummy ViewController
        oucon := TUIViewController.Wrap(TUIViewController.alloc.init);
        {Now we are creating a new Viewcontroller now when it is created
         it will have to check what is the supported orientations}
        oucon := win.rootViewController;//we store all our current content to the new ViewController
        Win.setRootViewController(viewController);
        Win.makeKeyAndVisible;// We display the Dummy viewcontroller
    
        win.setRootViewController(oucon);
        win.makeKeyAndVisible; 
        {And now we Display our original Content in a new Viewcontroller 
         with our new Supported orientations} 
    end;
    

    All you have to do now is Call ChangeiOSorientation(toOrientation,possibleOrientations)

    If you want it to go to Portrait but have landscape as a option

     ChangeiOSorientation(UIInterfaceOrientationPortrait,[TScreenOrientation.Portrait,TScreenOrientation.Landscape,TScreenOrientation.InvertedLandscape]);    
    

    This is working on

    • Delphi 10 Seattle
    • iPhone 5 running iOS 9.0
    • PA Server 17.0
    • Xcode 7.1
    • iPhoneOS 9.1 SDK