Locking orientation for ionic PWAs

Christian Rosenbauer
7 min readMar 10, 2021

This article covers Screen Orientation for a PWA (Progressive Web Application). If you are developing a native ionic app there is already an easy to use Cordova plugin.

Please remember that this article covers relatively new features as of March 2021. Things are subject to change in the future. But first of all here is a demo of the end result: a nice crossplatform orientation lock made in Ionic Angular.

Demo: https://ionic-orientation-service.stackblitz.io/

Introduction

Locking the screen orientation is a rather basic functionality one would think. Watching videos, viewing landscape images, using interactive fullscreen sliders — the possibilities for making use of a screen lock for a better user experience are near endless. Since a screen lock is a very valueable feature the Screen Orientation API has been invented to make your life easy.

Well then! Let’s take a look at how good it is being covered by typical browsers:

… Well at least the API should make your life easy. Although the Screen Orientation API is already widely supported, Apple doesn’t support it and we need to deal with it. We could dynamically check for the current aspect ratio and force the user to rotate the screen as a fallback solution. In this case we might as well make use of another great feature: The Full Screen API.

Technically a locked screen is just a fullscreen view with a forced orientation. So combining a message to the user to turn the device with a switch to fullscreen seems like a great fallback to the Screen Orientation API. Also for Screen Orientation API’s lock function to work the App has to be in fullscreen anyway. How well is the support for Fullscreen API then?

Well, it seems we are getting there! 👌

Even if the Full Screen API is not perfectly supported we can still make it work as a fallback option. There is a twist however specifically for Safari on iOS that is quite bothersome: Fullscreen is only supported on iPads and not on iPhones.

Without support for the Screen Lock API and the Fullscreen API we are out of luck when it comes to iPhones specifically. There is also another important detail for both APIs: they can only be used if there has been a user interaction before. So whenever we want a forced screen orientation we have to make sure that this action is connected to a interaction by the user.

Drafting the logic

Let’s sum up what we’ve learned so far:

  • The Screen Orientation API can be used for all devices except iOS devices.
  • The Fullsreen API will work on all devices excluding iPhones
  • Both locking the screen and switching to fullscreen is only possible if the user interacted with the App
  • For a screen lock to work the App has to be in fullscreen mode

I always recommend to conceptualize logic by taking all restrictions into account. This gives us a good idea of the basic logic that has to be covered.

The next important part is how we integrate that logic in the best possible way. For that I usually like to make a list of additional logic:

  1. (Un)locking the screen should happen within a service so that we can force a certain orientation whenever we want to anywhere in the app.
  2. We should listen for events that can occur whenever the user rotates the device or leaves and re-enters the app.
  3. If the device is locked and the orientation is wrong we show a modal that can only be dismissed if the orientation is correct.
  4. Dismissing the button will either make use of the Screen Orientation API or the Fullscreen API depending on support.

Our basic logic should be ready in theory now! Since dismissing a modal by a button is considered a user interaction we can safely activate one of the APIs without losing usability. By listening to events and checking the aspect ratio manually we can also force orientation without using any of the APIs — this wouldn’t trigger fullscreen but it’s not a big deal on a installed PWA anyway.

Let’s start coding

We start by creating a new service. This can be done by the ionic generate service command if you like to. I simply call our service OrientationService.

This might seem a quite specific for now but let me explain what we do here step by step. First let’s take a look at some of the variables:

currentOrientation$ — This is an Observable that represents our current Orientation.

lockedOrientation$ — This is either a BehaviorSubject of the type OrientationType or null when the orientation is not locked.

lockIsReady$ — A BehaviorSubject of the type boolean . This tells us if the screen lock is ready.

Hint: Variables that are observable should always end with a $ sign to follow Angulars naming conventions for Observables.

Making a reactive orientation check

Within the constructor we want to add the following EventListeners:

document.visibilitychange — this triggers whenever you leave/enter the app or switch tabs

document.fullscreenchange — this triggers whenever we go into or out of fullscreen mode.

window.resize — this triggers whenever the window resizes.

screen.change — this triggers whenever the orientation changes.

Now that we know what EventListeners we need we can let RxJS do it’s magic to check for orientation in an elegant way by using fromEvent, merge, and map operators.

Our events array contains many observables and we want to listen to all of them. That’s why we merge the entire array with the use of Spread syntax. We also apply the debounceTime operator to solve two problems at once:

  1. Many of these events fire at nearly the same time.
  2. Depending on the event the updated window size is not immediatly available.

With debounceTime we make sure that only the last value between the timespan gets emitted. Next we are using the map operator to run the checkOrientation() function since we don’t actually care about the values of the observables. What we care about is the fact that they can all be related to orientation changes.

We calculate window.outerWidth / window.outerHeight to get the aspect ratio. It’s fundamental that we use outerWidth/Height and not innerWidth/Height because the last one won’t work on iOS. iOS has it’s own way of changing orientation by rotating the inner window by 90°.

Setting the screen orientation

Now that we know how to check the screen orientation on all devices it’s time that we deal with the APIs that have been introduced before. Most of the following code should be pretty much self-explanatory.

setOrientation() either returns true or false depending on if the Screen Orientation APIs lock() function could be resolved. Before a lock can be applied the App also has to be in Fullscreen. For better compatibility the functions for toggling Fullscreen and Screen Lock go through browser prefix checks within a short circuit and they always resolve with a boolean value to not trigger any errors.

Locking and Unlocking

Our lock() function first pushes the orientation that we want to be locked to the lockedOrientation$ BehaviorSubject. We also subscribe to the currentOrientation$ Observable and pipe two operators.

With takeWhile we take values as long as the lockedOrientation$ BehaviorSubject’s value is not null. Should it be null the subscription would complete in which case the screen gets unlocked and we leave fullscreen mode.

distinctUntilChanged on the other hand ensures that we only listen to changes in orientation as only values that are different from the last value will be emitted.

Within the next notification we take a look at if the orientation is correct. For desktop devices there is no way to change the orientation — thats why true always gets emitted. We check if a modal is currently active and depending on that as well as the status of the orientation we either show the modal or emit true.

Making the modal

Now that our service is ready it’s time to add the final piece: a nice looking modal that request to rotate the screen.

With CSS animations we can make an easy to understand modal.

Our modal is a component that needs the OrientationService and our OrientationService also calls the OrientationModalComponent. Why is this important? Because if we separate these two and import each other it would cause circular dependency warnings.

Normally we strictly separate most things in Angular. However, the Modal we use is only used within our service and that’s why we can safely attach the code at the bottom to prevent circular dependency. It should be noted though that this is only ever a good idea in situations like these: One component that is only called within one service.

I don’t go into too much detail on how to design the modal — that’s up to you. All our modal truely has to do is to be only dismissed when the current orientation matches the locked orientation and we press a button. Because as you might remember from before only a user interaction can trigger Fullscreen and Screen Lock.

For this final part I am just going to put a StackBlitz here of the entire Service and Modal — it even includes JSDoc Comments to make your life as easy as possible. :)

--

--

Christian Rosenbauer
0 Followers

I am a developer from Germany who studied International Information Systems Management. Currently working on medical health applications.