viewport-fit=cover is confusing for app developers

November 30, 2017
Manabu Ueno

(Updated on Dec 8, 2017)

First, you know the Safari on iPhone X automatically applies paddings. The contents will be restricted inside the safe area by default.


Fig1. Safari with default paddings – portrait

Fig.2. Safari with default paddings – landscape

From web developer’s perspective, as Apple/ WebKit team mentions, you can exclude the auto-applied paddings to use the whole screen on iPhone X by declaring viewport-fit=cover within viewport meta element in the HTML.


Fig.3. Safari with viewport-fit=cover – portrait

Fig.4. Safari with viewport-fit=cover – landscape

From app developer’s perspective, WKWebView applies its contentInset (actually it is a property of webView.scrollView) as much as the safeAreaInsets, by default.


Fig.5. WKWebView with default paddings – portrait

Fig.6. WKWebView with default paddings – landscape

When a WKWebView instance loads a webpage which has that declaration, its scroll view (webView.scrollView) switches the contentInsetAdjustmentBehavior property from UIScrollViewContentInsetAdjustmentAlways to UIScrollViewContentInsetAdjustmentNever. This means that the web view takes amount of the safeAreaInsets paddings off from the contentInset, so that your contents go underneath the navigation bar, tool/tab bar, and the notch, at initial state.


Fig.7. WKWebView with viewport-fit=cover – portrait

Fig.8. WKWebView with viewport-fit=cover – landscape

However, this behavior is different from Safari, as Fig.3 and 4. On Safari, the content does not go underneath the navigation bar at initial state, even you are declaring viewport-fit=cover.

Originally UIView can have its safeAreaInsets for all four edges, and UIScrollView’s contentInsetAdjustmentBehavior property also affects four edges. When you have a navigation bar and a tool/tab bar then place a WKWebView instance as filling the screen with setting the contentInsetAdjustmentBehavior to UIScrollViewContentInsetAdjustmentNever, you will see the content goes under the navigation bar and the tool/tab bar like fig.7 and 8. This is what you expect, and this is exactly what happens when you declare viewport-fit=cover in the HTML with WKWebView.

So why Safari looks different? Is it because Safari’s web view is placed below the navigation bar (so the contentInset.top is always 0)? No, you can tell it is placed filling the whole screen because when you scroll a page, the content goes up underneath the navigation bar, and you can see it through the translucent navigation bar (in portrait).

OK, it seems like – Safari’s web view is placed filling the screen, and it always do have contentInset.top as much as the navigation bar height, but actually it always has no safeAreaInset.top which is represented by env(safe-area-inset-top), no matter if you declare viewport-fit=cover or not. In other words, even when you declare viewport-fit=cover in the HTML, Safari does not take amount of safeAreaInsets.top off from its contentInset.top, while it is taking safeAreaInsets off from other 3 edges (left, right, bottom).

That is why the content does not go under the navigation bar at initial state on Safari even if you declare viewport-fit=cover.

For example, apple.com home page is declaring viewport-fit=cover but does not seem to be applying env(safe-area-inset-top) to its header bar. Safari displays it appropriately. However, on my custom web browser made with WKWebView, the header bar goes under the navigation bar inappropriately and is snapped onto the top edge of the screen (WKWebView). It is because the viewport-fit=cover declaration makes the web view remove paddings on all four edges. On Safari, the top padding (contentInset.top) still remains under viewport-fit=cover condition.


Fig.9. apple.com on Safari – portrait

Fig.10. apple.com on WKWebView – portrait

Therefore, if you implement your custom web browser with WKWebView, you should do the following things.

  1. Place a WKWebView instance to fill the screen, spread underneath navigation bar and tool/tab bar.
  2. Set webView.scrollView.delegate, and in its scrollViewDidChangeAdjustedContentInset delegate method, check contentInsetAdjustmentBehavior of the scrollView. if it is UIScrollViewContentInsetAdjustmentAlways (this is the default condition), set the contentInset as (0, 0, 0, 0). The actual contentInset is automatically adjusted to safeAreaInsets. If the contentInsetAdjustmentBehavior is UIScrollViewContentInsetAdjustmentNever (this means the webpage has viewport-fit=cover declaration), set the contentInset as (superView.safeAreaInsets.top, 0, 0, 0). The content area of the web view spreads to fill the screen except the top edge, it will not hide the top part of your contents under navigation bar at the initial state.
  3. You should update the contentInset values when the device is rotated because the safe area will be changed on rotation. However the safeAreaInset is updated after the rotation completes, so that I think it is fine to update your contentInset in the viewController’s viewWillLayoutSubviews method.
  4. At this point, the content will be displayed appropriately if it is not applied env(safe-area-inset-top). If it is applied, however, the content gets unwanted extra top space because env(safe-area-inset-top) still returns some value that represents safeAreaInsets.top even though the content is not overlapped by navigation bar, because WKWebView is placed filling the screen and overlapped by navigation bar. To eliminate the extra space, you need to change the value of env(safe-area-inset-top) by overriding WKWebView’s safeAreaInsets getter. On a sub-classed WKWebView, add safeAreaInsets getter which returns UIEdgeInsets as (0, super.safeAreaInsets.left, super.safeAreaInsets.bottom, super.safeAreaInsets.right) if its scrollView.contentInsetAdjustmentBehavior is UIScrollViewContentInsetAdjustmentNever. If it is UIScrollViewContentInsetAdjustmentAlways, return (super.safeAreaInsets.top, super.safeAreaInsets.left, 0, super.safeAreaInsets.right).
  5. Modify webView.scrollView.scrollIndicatorInsets appropriately for each condition.

With those implementations, the custom web browser behaves like Safari.


Fig.11. Fixed WKWebView – portrait

Fig.12. Fixed WKWebView – landscape

Fig.13. apple.com on Fixed WKWebView

The confusing point for app developers is – WKWebView automatically responds to viewport-fit=cover declaration and the contentInsetAdjustmentBehavior property is switched to UIScrollViewContentInsetAdjustmentNever. This removes auto-applied paddings from all four edges. However, the top edge still needs its padding (contentInset.top) as much as the amount of safeAreaInsets.top to display the content avoiding the navigation bar at initial state, even env(safe-area-inset-top) is and is not applied. So you need to care about it by yourself.

P.S.
Though, WKWebView has a bug on rotation. When it is displaying a web page which has viewport-fit=cover declaration, the layout is collapsed by rotating device. This is another story.

Comments are closed.