Search code examples
iosscreensafearea

Detect screen notch from prefersStatusBarHidden


There are many question on StackOverflow about detecting whether an iPhone device has a notch in the screen, such as this one. The answers almost always recommend using the safeAreaInsets property of the top window. I have used that in my app to determine whether a status bar should be shown, from the prefersStatusBarHidden method of the currently shown view controller. I would like to show the status bar when there is a notch, but not when not. It works fine in all my tests, but for some customers the status bar disappears sometimes, even though they use a device with a notch (an iPhone 12 Pro Max).

I looked into it, and I think the problem is probably caused by a recursive call to safeAreaInsets, see following call-stack:

enter image description here

It makes some sense. In order to determine how large the safe area needs to be, iOS needs to know whether the status bar needs to be shown. And therefore, it calls prefersStatusBarHidden of the visible view controller, which then uses the safe area to determine that....

Despite the recursive call, it still works for me in tests, but as said, it fails sometimes for some users. I need to work with prefersStatusBarHidden, because at top-level the app consists of a UITabBarController, for which only one tab hides the status bar. The other tabs should always show the status bar, independent of whether there is notch or not.

I have considered checking the device type with sysctlbyname with "hw.machine" argument, and then use a mapping table to get the notch/no-notch result. But that has the disadvantage that the mapping table would need to be updated for each new iPhone model, and that it doesn't work on the simulator, which always returns the Mac machine name.

Any ideas how to solve that in a better way? I could simply avoid the recursive call, but will that solve the problem?


My code that determines the notch right now (Objective-C):

- (bool) hasTopNotch
{
    if (@available(iOS 11.0, *)) {
        UIWindow *window = [UIApplication sharedApplication].delegate.window;
        UIEdgeInsets insets = window.safeAreaInsets;
        return insets.top >= 44;
    } else {
        return NO;
    }
}

Solution

  • I'm not familiar with Obj-c but that looks like a computed property/function. Every time you access it, it will get the current safe area inset and return a Bool.

    But the problem is that you are then setting prefersStatusBarHidden based on that Bool. If the status bar is hidden, the safe area will get smaller. Then, the next time you access the hasTopNotch property, it will return an incorrect value.

    Instead, what I do is check the safe area once and only once the app starts. Your user's device isn't ever going to change, so you don't need a function. In Swift:

    var deviceHasNotch = false /// outside any class
    
    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
        var window: UIWindow?
        func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
            deviceHasNotch = window?.safeAreaInsets.bottom ?? 0 > 0 /// set it here
        }
    }