safeAreaInsets in UIView is 0 on an iPhone X
Asked Answered
L

14

83

I am updating my app to adapt it for iPhone X. All views work fine by now except one. I have a view controller that presents a custom UIView that covers the whole screen. Before I was using UIScreen.main.bounds to find out the size of the view before all layout was done (I need it for putting the correct itemSize for a collectionView). I thought that now I could do something like UIScreen.main.bounds.size.height - safeAreaInsets.bottom to get the right usable size. The problem is, safeAreaInsets returns (0,0,0,0) trying on an iPhone X (Simulator). Any ideas? In other views, I get the right numbers for safeAreaInsets.

Thank you!

Langdon answered 31/10, 2017 at 10:16 Comment(2)
Anywhere, anytime, you can use: ` if #available(iOS 11.0, tvOS 11.0, *) { return UIApplication.shared.delegate?.window??.safeAreaInsets != .zero } return false` SOURCESunbow
As per Mars answer below, I'm also seeing that the only reliable way to get these is in viewDidLayoutSubviews() or viewDidAppear. Actually, "override func viewSafeAreaInsetsDidChange()" seems to work best.Amalia
L
48

I already figure out the solution: I was doing all the implementation in the init of the view. safeAreaInsets has the correct size in layoutSubviews()

Langdon answered 31/10, 2017 at 10:24 Comment(3)
If you're using view in a UIViewController, you can also do it in viewDidLayoutSubviews() and it should work as it did for me.Tangent
Be aware of being on viewWillLayoutSubviews subviews have wrong saveAreaInsets value.Woodbury
You can also do it in UIViewController.viewDidLayoutSubviews.Stephi
P
106

I recently had a similar problem where the safe area insets are returning (0, 0, 0, 0) as soon as viewDidLoad is triggered. It seems that they are set fractionally later than the rest of the view loading.

I got round it by overriding viewSafeAreaInsetsDidChange and doing my layout in that instead:

override func viewSafeAreaInsetsDidChange() {
 // ... your layout code here
} 
Pollinate answered 18/5, 2018 at 20:5 Comment(5)
ditto - this is correct - despite it being a bit dumb that this is the caseDuwalt
If the OP wants to do this in the custom view instead of the view controller, then there's also the UIView method safeAreaInsetsDidChange.Impresa
This is perfect if you're configuring your cells in a viewcontrollerArtema
I've changed to landscape mode and this is the only answer that worked for me!Stallworth
Works like a charm. Not sure how I even survived the past 5 years not knowing about this method :/Langille
L
48

I already figure out the solution: I was doing all the implementation in the init of the view. safeAreaInsets has the correct size in layoutSubviews()

Langdon answered 31/10, 2017 at 10:24 Comment(3)
If you're using view in a UIViewController, you can also do it in viewDidLayoutSubviews() and it should work as it did for me.Tangent
Be aware of being on viewWillLayoutSubviews subviews have wrong saveAreaInsets value.Woodbury
You can also do it in UIViewController.viewDidLayoutSubviews.Stephi
M
43

I've run into this issue too trying to move up views to make way for the keyboard on the iPhone X. The safeAreaInsets of the main view are always 0, even though I know the subviews have been laid out at this point as the screen has been drawn. A work around I found, as and mentioned above, is to get the keyWindow and check its safe area insets instead.

Obj-C:

CGFloat bottomInset = [UIApplication sharedApplication].keyWindow.safeAreaInsets.bottom;

Swift:

let bottomInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom

You can then use this value to adjust constraints or view frames as required.

Magnetite answered 18/5, 2018 at 11:29 Comment(3)
It works on portrait mode. The landscape doesn't have the view on top.Lettering
only this solution worked for me. viewSafeAreaInsetsDidChange() not called sometimes at all.Circumnavigate
Where do I add bottomInset then?Columnar
V
21

I have a view which is a subview inside another view. I found that I can't get safeAreaInsets correctly, it always return 0, in that view on iPhoneX even if I put it in layoutSubviews. The final solution is I use following UIScreen extension to detect safeAreaInsets which can work like a charm.

extension UIScreen {

    func widthOfSafeArea() -> CGFloat {

        guard let rootView = UIApplication.shared.keyWindow else { return 0 }

        if #available(iOS 11.0, *) {

            let leftInset = rootView.safeAreaInsets.left

            let rightInset = rootView.safeAreaInsets.right

            return rootView.bounds.width - leftInset - rightInset

        } else {

            return rootView.bounds.width

        }

    }

    func heightOfSafeArea() -> CGFloat {

        guard let rootView = UIApplication.shared.keyWindow else { return 0 }

        if #available(iOS 11.0, *) {

            let topInset = rootView.safeAreaInsets.top

            let bottomInset = rootView.safeAreaInsets.bottom

            return rootView.bounds.height - topInset - bottomInset

        } else {

            return rootView.bounds.height

        }

    }

}
Vd answered 15/12, 2017 at 11:30 Comment(5)
While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value.Meany
Thank you for your suggestion. I have already edited my answer.Vd
I found that when manually adding subviews (even if they use autolayout), in some cases, like table view controllers, safeAreaInsets is always zero. Using the application key window returns the correct safe area insets. Good call.Helotism
This doesn't work on iOS 11 when there is just a status bar (iPhone 6,7,...). It will return 0 for the top inset heightBoigie
You could make them staticUrsa
F
15

I try to use "self.view.safeAreaInset" in a view controller. First, it is a NSInsetZero when I use it in the controller's life cycle method "viewDidLoad", then I search it from the net and get the right answer, the log is like:

ViewController loadView() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewDidLoad() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewWillAppear() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewDidLayoutSubviews() SafeAreaInsets :UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
ViewController viewDidAppear() SafeAreaInsets :UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)

so you can choice the right method that you need the safeAreaInset and use it!

Friedrich answered 6/12, 2019 at 8:5 Comment(0)
C
9

Swift iOS 11,12,13+

var insets : UIEdgeInsets = .zero

 override func viewDidLoad() {
     super.viewDidLoad()

     insets = UIApplication.shared.delegate?.window??.safeAreaInsets ?? .zero
     //Or you can use this
     insets = self.view.safeAreaInsets
}
Cath answered 22/1, 2020 at 7:14 Comment(1)
Do we know why UIApplication.shared.keyWindow stopped returning the safe area insets? Seems risky to use UIApplication.shared.delegate?.window??.safeAreaInsets if we can't be sure this will continue to work into the future. I am assuming this is because of the addition of the Scenes, but they didn't appear until iOS 13 so maybe not?Unsteady
R
4

In my case I was adding a UICollectionView inside viewDidLoad()

collectionView = UICollectionView(frame: view.safeAreaLayoutGuide.layoutFrame, collectionViewLayout: createCompositionalLayout())

Unfortunately at this stage safeAreaLayoutGuide is still zero.

I solved it by adding:

override func viewDidLayoutSubviews() {
    collectionView.frame = view.safeAreaLayoutGuide.layoutFrame
}
Rodrique answered 8/7, 2020 at 9:59 Comment(0)
R
2

the viewDidAppear(_:) method of the container view controller that extends the safe area of its embedded child view controller to account for the views in .

Make your modifications in this method because the safe area insets for a view are not accurate until the view is added to a view hierarchy.

override func viewDidAppear(_ animated: Bool) {

   if (@available(iOS 11, *)) {

     var newSafeArea = view.safeAreaInsets

     // Adjust the safe area to accommodate 
     //  the width of the side view.

     if let sideViewWidth = sideView?.bounds.size.width {
        newSafeArea.right += sideViewWidth
     }

    // Adjust the safe area to accommodate 
    //  the height of the bottom view.
    if let bottomViewHeight = bottomView?.bounds.size.height {
       newSafeArea.bottom += bottomViewHeight
    }

    // Adjust the safe area insets of the 
    //  embedded child view controller.
    let child = self.childViewControllers[0]
    child.additionalSafeAreaInsets = newSafeArea
  } 
}
Reagan answered 31/10, 2017 at 10:30 Comment(0)
F
2

I've come across the same problem. In my case the view I'm inserting would be sized correctly after calling view.layoutIfNeeded(). The view.safeAreaInsets was set after this, but only the top value was correct. The bottom value was still 0 (this on an iPhone X).

While trying to figure out at what point the safeAreaInsets are set correctly, I've added a breakpoint on the view's safeAreaInsetsDidChange() method. This was being called multiple times, but only when I saw CALayer.layoutSublayers() in the backtrace the value had been set correctly.

So I've replaced view.layoutIfNeeded() by the CALayer's counterpart view.layer.layoutIfNeeded(), which resulted in the safeAreaInsets to be set correctly right away, thus solving my problem.

TL;DR

Replace

view.layoutIfNeeded()

by

view.layer.layoutIfNeeded()
Fishbolt answered 12/1, 2018 at 17:35 Comment(1)
Not working, not useful if you are animating a view transtion.Vevina
P
1
[UIApplication sharedApplication].keyWindow.safeAreaInsets return none zero
Philcox answered 19/3, 2019 at 5:49 Comment(0)
C
1

Just try self.view.safeAreaInsets instead of UIApplication.shared.keyWindow?.safeAreaInsets Safe area insets seems to not fill on iOS 11.x.x devices when requested via application keyWindow.

Circumnavigate answered 15/4, 2019 at 12:52 Comment(0)
S
1

View layout is never guaranteed until layoutSubviews or viewDidLayoutSubviews. Never rely on sizes before these lifecycle methods. You will get inconsistent results if you do.

Scaremonger answered 12/11, 2019 at 22:32 Comment(0)
L
1

To calculate safe area safeAreaInsets, try to obtain it in viewWIllAppear(), as in didLoad() the view have not been formed. You will have the correct inset in willAppear!

Lexicography answered 14/11, 2019 at 5:6 Comment(0)
N
0

In case you cannot subclass, you can use this UIView extension.

It gives you an API like this:

view.onSafeAreaInsetsDidChange = { [unowned self] in
   self.updateSomeLayout()
}

The extension adds an onSafeAreaInsetsDidChange property using object association. Then swizzles the UIView.safeAreaInsetsDidChange() method to call the closure (if any).

extension UIView {
    
    typealias Action = () -> Void
    
    var onSafeAreaInsetsDidChange: Action? {
        get {
            associatedObject(for: "onSafeAreaInsetsDidChange") as? Action
        }
        set {
            Self.swizzleSafeAreaInsetsDidChangeIfNeeded()
            set(associatedObject: newValue, for: "onSafeAreaInsetsDidChange")
        }
    }
    
    static var swizzled = false
    
    static func swizzleSafeAreaInsetsDidChangeIfNeeded() {
        guard swizzled == false else { return }
        swizzle(
            method: "safeAreaInsetsDidChange",
            originalSelector: #selector(originalSafeAreaInsetsDidChange),
            swizzledSelector: #selector(swizzledSafeAreaInsetsDidChange),
            for: Self.self
        )
        swizzled = true
    }
    
    @objc func originalSafeAreaInsetsDidChange() {
        // Original implementaion will be copied here.
    }
    
    @objc func swizzledSafeAreaInsetsDidChange() {
        originalSafeAreaInsetsDidChange()
        onSafeAreaInsetsDidChange?()
    }
}

It uses some helpers (see NSObject+Extensions.swift and NSObject+Swizzle.swift), but you don't really need it if you use sizzling and object association APIs directly.

Necessity answered 6/4, 2022 at 10:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.