Thanks to both @pascalzon and @YYL for useful information. I tried switching to WKWebView for my Cordova app but it just broke it, so for the time being I am stuck with uiWebview and thus this issue.
I want my app to look nice on notched devices like the iPhone X so I added viewport-fit=cover
to the app's viewport declaration and started playing with safe-area-insets. My layout needs are quite simple. The top margin must either be 1rem or safe-area-inset-top if this has a value greater than 1rem.
Unfortunately, the CSS max() function is not yet standard. If I could have used that things would have been simpler (assuming it converts everything to pixels).
:root {
--app-margin-top-default: 1rem;
--app-margin-top: max(env(safe-area-inset-top), var(--app-margin-top-default));
}
So I was forced to perform the max comparison in javascript and then set --app-margin-top
from there each time the screen orientation changed.
Since there seems to be no way, as yet, of reading CSS env vars from javascript, I start in the CSS by recording safe area insets in CSS variables which I can access later from javascript. Default values are:
:root {
--safe-area-inset-top: 0px;
--safe-area-inset-right: 0px;
--safe-area-inset-bottom: 0px;
--safe-area-inset-left: 0px;
}
I then set these vars as follows:
/* iOS 11.0: supports constant() css function. (Assume all other inset vars are supported.) */
@supports (padding-top: constant(safe-area-inset-top)) {
:root {
--safe-area-inset-top: constant(safe-area-inset-top, 0);
--safe-area-inset-right: constant(safe-area-inset-right, 0);
--safe-area-inset-bottom: constant(safe-area-inset-bottom, 0);
--safe-area-inset-left: constant(safe-area-inset-left, 0);
}
}
/* iOS 11.2 and latest Chrome webviews support the env() css function. (Assume all other inset vars are supported.) */
@supports (padding-top: env(safe-area-inset-top)) {
:root {
--safe-area-inset-top: env(safe-area-inset-top, 0);
--safe-area-inset-right: env(safe-area-inset-right, 0);
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0);
--safe-area-inset-left: env(safe-area-inset-left, 0);
}
}
Each time the screen orientation changed I would then compare --safe-area-inset-top
with --app-margin-top-default
and set --app-margin-top
to the maximum of these values. It was then that I ran into this problem. There seemed to be a lag between the screen orientation changing and my CSS safe area inset variables being updated. So my margin setting code did not work.
I tried YYL's trick to force recalculation of the safe area insets but it didn't seem to work. The insets were eventually updated but after my screen orientation change event listener had finished executing :(
My Solution
This solution relies on the cordova screen orientation plugin:
https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-screen-orientation/
The only time you can rely on the safe area inset values being right is at application start up, so I record them in a set of javascript variables inside my app
object:
safeAreaInsetNorth: undefined,
safeAreaInsetEast: undefined,
safeAreaInsetSouth: undefined,
safeAreaInsetWest: undefined,
The method that sets these variables at startup takes screen orientation into account.
console.log("Screen orientation at startup is: " +screen.orientation.type);
let $root = $(":root");
// Notch/North at the top.
if (screen.orientation.type.match("portrait-primary")) {
app.safeAreaInsetNorth = $root.css("--safe-area-inset-top");
app.safeAreaInsetEast = $root.css("--safe-area-inset-right");
app.safeAreaInsetSouth = $root.css("--safe-area-inset-bottom");
app.safeAreaInsetWest = $root.css("--safe-area-inset-left");
}
// Upside down... Notch/North at the bottom.
else if (screen.orientation.type.match("portrait-secondary")) {
app.safeAreaInsetNorth = $root.css("--safe-area-inset-bottom");
app.safeAreaInsetEast = $root.css("--safe-area-inset-left");
app.safeAreaInsetSouth = $root.css("--safe-area-inset-top");
app.safeAreaInsetWest = $root.css("--safe-area-inset-right");
}
// Notch/North to the left.
else if (screen.orientation.type.match("landscape-primary")) {
app.safeAreaInsetNorth = $root.css("--safe-area-inset-left");
app.safeAreaInsetEast = $root.css("--safe-area-inset-top");
app.safeAreaInsetSouth = $root.css("--safe-area-inset-right");
app.safeAreaInsetWest = $root.css("--safe-area-inset-bottom");
}
// Notch/North to the right.
else if (screen.orientation.type.match("landscape-secondary")) {
app.safeAreaInsetNorth = $root.css("--safe-area-inset-right");
app.safeAreaInsetEast = $root.css("--safe-area-inset-bottom");
app.safeAreaInsetSouth = $root.css("--safe-area-inset-left");
app.safeAreaInsetWest = $root.css("--safe-area-inset-top");
} else {
throw "I have no idea which way up I am!";
}
At startup and then again each time the screen orientation changes I check to see if I need to update --app-margin-top
like this:
let marginTopDefault = app.getGlobalCssVariableInPx("--app-margin-top-default");
let newMarginTop = undefined;
switch(screen.orientation.type) {
case "portrait-primary": // Notch/North at the top.
newMarginTop = app.safeAreaInsetNorth;
break;
case "portrait-secondary": // Upside down... Notch/North at the bottom.
newMarginTop = app.safeAreaInsetSouth;
break;
case "landscape-primary": // Notch/North to the left.
newMarginTop = app.safeAreaInsetEast;
break;
case "landscape-secondary": // Notch/North to the right.
newMarginTop = app.safeAreaInsetWest;
break;
default:
throw "No idea which way up I am!";
}
app.getGlobalCssVariableInPx
is a helper function that converts rems into pixels by multiplying them by the base font size.
I then convert marginTopDefault
and newMarginTop
to integers, find the max and set --app-margin-top
to this.
$(":root").css("--app-margin-top", Math.max(marginTopDefault, newMarginTop) + "px");
Conclusion
I think I have described this well enough. It was a lot of messing about to get something working that should have just worked out of the box, but hey ho. That's how it goes sometimes. And I am no HTML5 guru, so there are probably ways in which this could made simpler.