277

What would be the most proper way to get both top and bottom height for the unsafe areas?

enter image description here

stackich
  • 3,607
  • 3
  • 17
  • 41
Tulleb
  • 8,919
  • 8
  • 27
  • 55

24 Answers24

554

Try this :

In Objective C

if (@available(iOS 11.0, *)) {
    UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
    CGFloat topPadding = window.safeAreaInsets.top;
    CGFloat bottomPadding = window.safeAreaInsets.bottom;
}

In Swift

if #available(iOS 11.0, *) {
    let window = UIApplication.shared.keyWindow
    let topPadding = window?.safeAreaInsets.top
    let bottomPadding = window?.safeAreaInsets.bottom
}

In Swift - iOS 13.0+

// Use the first element from windows array as KeyWindow deprecated

if #available(iOS 13.0, *) {
    let window = UIApplication.shared.windows.first
    let topPadding = window.safeAreaInsets.top
    let bottomPadding = window.safeAreaInsets.bottom
}

In Swift - iOS 15.0+

// Use the keyWindow of the currentScene

if #available(iOS 15.0, *) {
    let window = UIApplication.shared.currentScene?.keyWindow
    let topPadding = window.safeAreaInsets.top
    let bottomPadding = window.safeAreaInsets.bottom
}
Tulleb
  • 8,919
  • 8
  • 27
  • 55
user6788419
  • 7,196
  • 1
  • 16
  • 28
128

To get the height between the layout guides you just do

let guide = view.safeAreaLayoutGuide
let height = guide.layoutFrame.size.height

So full frame height = 812.0, safe area height = 734.0

Below is the example where the green view has frame of guide.layoutFrame

enter image description here

Krishna Raj Salim
  • 7,331
  • 5
  • 34
  • 66
Ladislav
  • 7,223
  • 5
  • 27
  • 31
  • Thanks that was the lead I was also thinking about. I don't think there is a more reliable solution. But with this I only get the full unsafe height, right? Not two height for each areas? – Tulleb Oct 19 '17 at 14:45
  • 3
    Once views have been laid out this will give you the correct answer – Ladislav Oct 19 '17 at 14:47
  • 3
    Actually @user6788419's answer looks nice too and way shorter. – Tulleb Oct 19 '17 at 14:50
  • I'm getting a height of 812.0 with that code, using the view of a controller. So I'm using `UIApplication.sharedApplication.keyWindow.safeAreaLayoutGuide.layoutFrame`, which does have the safe frame. – Ferran Maylinch Mar 07 '18 at 12:05
  • 1
    @FerranMaylinch Not sure where you are checking the height, but I also had 812, which turned out to be due to checking the value before the view was visible. In that case the heights will be the same "If the view is not currently installed in a view hierarchy, or is not yet visible onscreen, the layout guide edges are equal to the edges of the view" https://developer.apple.com/documentation/uikit/uiview/2891102-safearealayoutguide?language=objc – Nicholas Harlen Jul 10 '18 at 03:58
  • good but `view.safeAreaLayoutGuide` won't be available in `viewDidLoad` (zeros), I need to get safe width earlier, `UIScreen.main.bounds.size.width` works in `viewDidLoad` but it's full width, I need safe width – user924 May 02 '19 at 20:44
  • here's a solution which will work even in `viewDidLoad`: https://stackoverflow.com/a/53864017/7767664 – user924 May 02 '19 at 20:51
  • Thanks for your solution. Awsome colors:) They look incredible – the image is really huge. – Vyacheslav Apr 06 '20 at 23:19
109

Swift 4, 5

To pin a view to a safe area anchor using constraints can be done anywhere in the view controller's lifecycle because they're queued by the API and handled after the view has been loaded into memory. However, getting safe-area values requires waiting toward the end of a view controller's lifecycle, like viewDidLayoutSubviews().

This plugs into any view controller:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    let topSafeArea: CGFloat
    let bottomSafeArea: CGFloat

    if #available(iOS 11.0, *) {
        topSafeArea = view.safeAreaInsets.top
        bottomSafeArea = view.safeAreaInsets.bottom
    } else {
        topSafeArea = topLayoutGuide.length
        bottomSafeArea = bottomLayoutGuide.length
    }

    // safe area values are now available to use
}

I prefer this method to getting it off of the window (when possible) because it’s how the API was designed and, more importantly, the values are updated during all view changes, like device orientation changes.

However, some custom presented view controllers cannot use the above method (I suspect because they are in transient container views). In such cases, you can get the values off of the root view controller, which will always be available anywhere in the current view controller's lifecycle.

anyLifecycleMethod()
    guard let root = UIApplication.shared.keyWindow?.rootViewController else {
        return
    }
    let topSafeArea: CGFloat
    let bottomSafeArea: CGFloat

    if #available(iOS 11.0, *) {
        topSafeArea = root.view.safeAreaInsets.top
        bottomSafeArea = root.view.safeAreaInsets.bottom
    } else {
        topSafeArea = root.topLayoutGuide.length
        bottomSafeArea = root.bottomLayoutGuide.length
    }

    // safe area values are now available to use
}
trndjc
  • 11,654
  • 3
  • 38
  • 51
  • 3
    Please declare using let topSafeArea: CGFloat instead! – Gusutafu Apr 16 '18 at 17:38
  • avoid using `viewDidLayoutSubviews` (which can be called multiple times), here's a solution which will work even in `viewDidLoad`: https://stackoverflow.com/a/53864017/7767664 – user924 May 02 '19 at 20:50
  • @user924 then the values won’t be updated when the safe areas change (i.e. the double-height status bar) – trndjc May 02 '19 at 20:54
  • @bsod then this will be better https://stackoverflow.com/a/3420550/7767664 (because it's only for status bar and not other views) – user924 May 02 '19 at 20:56
  • Or instead of going through the app delegate you can just use the safe area API Apple built into the view controller. – trndjc May 02 '19 at 21:18
  • Your first example works for me in my GameViewControler, which launches GameScene. It's in the latter that I can't use it since it an SKScene and doesn't have the viewDidLayoutSubviews method. – Caractacus Oct 13 '20 at 03:41
  • `safeAreaInsets` are available both in `viewWillLayoutSubviews` and `viewDidLayoutSubviews`. Any reasons not to do it a little earlier? – Legonaftik May 13 '22 at 14:28
  • @Legonaftik it just depends on what you need done. If you have no use of the state of the UI after the subviews have been laid out then `viewWillLayoutSubviews` is totally suitable. At this point it's just a matter of personal preference. – trndjc May 14 '22 at 20:02
34

None of the other answers here worked for me, but this did.

var topSafeAreaHeight: CGFloat = 0
var bottomSafeAreaHeight: CGFloat = 0

  if #available(iOS 11.0, *) {
    let window = UIApplication.shared.windows[0]
    let safeFrame = window.safeAreaLayoutGuide.layoutFrame
    topSafeAreaHeight = safeFrame.minY
    bottomSafeAreaHeight = window.frame.maxY - safeFrame.maxY
  }
ScottyBlades
  • 12,189
  • 5
  • 77
  • 85
  • 2
    You code will work in any part of the view controller life cycle. If you want to use view.safeAreaInsets.top, then you need to make sure that you call it after in viewDidAppear or later. In viewDidLoad, you will not get the view.safeAreaInsets.top with the correct value because it is not initialised yet. – user3204765 Apr 06 '19 at 16:26
  • 3
    best answer so far – Rikco Dec 12 '19 at 09:40
  • 1
    This works for me too, for my case I have the `WKWebView` in the `UITableViewCell`'s `contentView` (subview), its height constraint was not calculated properly after app launched from background. Tried using `UIApplications`'s `safeAreaInsets` it did not return the correct top and bottom value. I even tried to reload the tableview in the `viewDidAppear`, didn't work as well. Thanks. – Dan Aug 24 '20 at 05:14
  • Good thinking, using `UIWindow` instead of `ViewController` properties so that it works in `viewDidLoad`. `UIWindow` also has a `safeAreaInsets` property so you can turn this code into one line by using `UIApplication.shared.windows[0].safeAreaInsets.top` and `UIApplication.shared.windows[0].safeAreaInsets.bottom` – Michael Mar 14 '22 at 17:18
33

All of the answers here are helpful, Thanks to everyone who offered help.

However as i see that that the safe area topic is a little bit confused which won’t appear to be well documented.

So i will summarize it here as mush as possible to make it easy to understand safeAreaInsets, safeAreaLayoutGuide and LayoutGuide.

In iOS 7, Apple introduced the topLayoutGuide and bottomLayoutGuide properties in UIViewController, They allowed you to create constraints to keep your content from being hidden by UIKit bars like the status, navigation or tab bar It was possible with these layout guides to specify constraints on content, avoiding it to be hidden by top or bottom navigation elements (UIKit bars, status bar, nav or tab bar…).

So for example if you wanna make a tableView starts from the top screen you have done something like that:

self.tableView.contentInset = UIEdgeInsets(top: -self.topLayoutGuide.length, left: 0, bottom: 0, right: 0)

In iOS 11 Apple has deprecated these properties replacing them with a single safe area layout guide

Safe area according to Apple

Safe areas help you place your views within the visible portion of the overall interface. UIKit-defined view controllers may position special views on top of your content. For example, a navigation controller displays a navigation bar on top of the underlying view controller’s content. Even when such views are partially transparent, they still occlude the content that is underneath them. In tvOS, the safe area also includes the screen’s overscan insets, which represent the area covered by the screen’s bezel.

Below, a safe area highlighted in iPhone 8 and iPhone X-series:

enter image description here

The safeAreaLayoutGuide is a property of UIView

To get the height of safeAreaLayoutGuide:

extension UIView {
   var safeAreaHeight: CGFloat {
       if #available(iOS 11, *) {
        return safeAreaLayoutGuide.layoutFrame.size.height
       }
       return bounds.height
  }
}

That will return the height of the Arrow in your picture.

Now, what about getting the top "notch" and bottom home screen indicator heights?

Here we will use the safeAreaInsets

The safe area of a view reflects the area not covered by navigation bars, tab bars, toolbars, and other ancestors that obscure a view controller's view. (In tvOS, the safe area reflects the area not covered by the screen's bezel.) You obtain the safe area for a view by applying the insets in this property to the view's bounds rectangle. If the view is not currently installed in a view hierarchy, or is not yet visible onscreen, the edge insets in this property are 0.

The following will show the unsafe area and there distance from edges on iPhone 8 and one of iPhone X-Series.

enter image description here

enter image description here

Now, if navigation bar added

enter image description here

enter image description here

So, now how to get the unsafe area height? we will use the safeAreaInset

Here are to solutions however they differ in an important thing,

First One:

self.view.safeAreaInsets

That will return the EdgeInsets, you can now access the top and the bottom to know the insets,

Second One:

UIApplication.shared.windows.first{$0.isKeyWindow }?.safeAreaInsets

The first one you are taking the view insets, so if there a navigation bar it will be considered , however the second one you are accessing the window's safeAreaInsets so the navigation bar will not be considered

stackich
  • 3,607
  • 3
  • 17
  • 41
mojtaba al moussawi
  • 1,360
  • 1
  • 13
  • 21
  • UIApplication.shared.windows.first{$0.isKeyWindow }?.safeAreaInsets this line is the best answer I believe – Mashhadi Mar 21 '23 at 15:32
10

Swift 5, Xcode 11.4

`UIApplication.shared.keyWindow` 

It will give deprecation warning. ''keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes' because of connected scenes. I use this way.

extension UIView {

    var safeAreaBottom: CGFloat {
         if #available(iOS 11, *) {
            if let window = UIApplication.shared.keyWindowInConnectedScenes {
                return window.safeAreaInsets.bottom
            }
         }
         return 0
    }

    var safeAreaTop: CGFloat {
         if #available(iOS 11, *) {
            if let window = UIApplication.shared.keyWindowInConnectedScenes {
                return window.safeAreaInsets.top
            }
         }
         return 0
    }
}

extension UIApplication {
    var keyWindowInConnectedScenes: UIWindow? {
        return windows.first(where: { $0.isKeyWindow })
    }
}
user1039695
  • 981
  • 11
  • 20
9

In iOS 11 there is a method that tells when the safeArea has changed.

override func viewSafeAreaInsetsDidChange() {
    super.viewSafeAreaInsetsDidChange()
    let top = view.safeAreaInsets.top
    let bottom = view.safeAreaInsets.bottom
}
Martin
  • 191
  • 2
  • 4
7

This works for the entire view life cycle as a simple 2-line solution in Swift:

let top    = UIApplication.shared.windows[0].safeAreaInsets.top
let bottom = UIApplication.shared.windows[0].safeAreaInsets.bottom

I personally needed it in the viewDidLoad and view.safeAreaInsets isn't calculated yet.

Michael
  • 748
  • 8
  • 11
  • This works for me, but I get the depracated warning -'windows' was deprecated in iOS 15.0: Use UIWindowScene.windows on a relevant window scene instead. - Unfortunately, the Apple hint is not very clear to me. – Markv07 Apr 08 '23 at 13:53
3

safeAreaLayoutGuide When the view is visible onscreen, this guide reflects the portion of the view that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. (In tvOS, the safe area reflects the area not covered the screen's bezel.) If the view is not currently installed in a view hierarchy, or is not yet visible onscreen, the layout guide edges are equal to the edges of the view.

Then to get the height of the red arrow in the screenshot it's:

self.safeAreaLayoutGuide.layoutFrame.size.height
malhal
  • 26,330
  • 7
  • 115
  • 133
3

Swift 5 Extension

This can be used as a Extension and called with: UIApplication.topSafeAreaHeight

extension UIApplication {
    static var topSafeAreaHeight: CGFloat {
        var topSafeAreaHeight: CGFloat = 0
         if #available(iOS 11.0, *) {
               let window = UIApplication.shared.windows[0]
               let safeFrame = window.safeAreaLayoutGuide.layoutFrame
               topSafeAreaHeight = safeFrame.minY
             }
        return topSafeAreaHeight
    }
}

Extension of UIApplication is optional, can be an extension of UIView or whatever is preferred, or probably even better a global function.

Charles
  • 105
  • 1
  • 9
3

For SwiftUI:

Code

private struct SafeAreaInsetsKey: EnvironmentKey {
    static var defaultValue: EdgeInsets {
        guard let scene = UIApplication.shared.connectedScenes.first,
              let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate,
              let window = windowSceneDelegate.window,
              let safeAreaInsets = window?.safeAreaInsets else {
          return .zero
        }

        return safeAreaInsets
    }
}

extension EnvironmentValues {
    var safeAreaInsets: EdgeInsets {
        self[SafeAreaInsetsKey.self]
    }
}

private extension UIEdgeInsets {
    var insets: EdgeInsets {
        EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
    }
}

Usage

struct MyView: View {
    
    @Environment(\.safeAreaInsets) private var safeAreaInsets
    
    var body: some View {
        Text("Ciao")
            .padding(safeAreaInsets)
    }
}
Tulleb
  • 8,919
  • 8
  • 27
  • 55
Lorenzo Fiamingo
  • 3,251
  • 2
  • 17
  • 35
2

I'm working with CocoaPods frameworks and in case UIApplication.shared is unavailable then I use safeAreaInsets in view's window:

if #available(iOS 11.0, *) {
    let insets = view.window?.safeAreaInsets
    let top = insets.top
    let bottom = insets.bottom
}
Tai Le
  • 8,530
  • 5
  • 41
  • 34
2
UIWindow *window = [[[UIApplication sharedApplication] delegate] window]; 
CGFloat fBottomPadding = window.safeAreaInsets.bottom;
Peter
  • 1,061
  • 1
  • 13
  • 20
2

Or using SwiftUI GeometryReader api.

struct V: View {
    var body: some View {
        GeometryReader { geometry in
            Text("\(geometry.safeAreaInsets.top)")
            Text("\(geometry.safeAreaInsets.bottom)")
        }
    }
}


Code might not work, but illustrates the idea.

Ilya
  • 1,349
  • 11
  • 16
1

Objective-C Who had the problem when keyWindow is equal to nil. Just put the code above in viewDidAppear (not in viewDidLoad)

1

For iOS 13+/Swift 5, nothing else here worked for me but this:

    if #available(iOS 13.0, *) {
        topPadding = UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0
        bottomPadding = UIApplication.shared.windows.first?.safeAreaInsets.bottom ?? 0
    }
Japes
  • 393
  • 5
  • 11
0

A more rounded approach

  import SnapKit

  let containerView = UIView()
  containerView.backgroundColor = .red
  self.view.addSubview(containerView)
  containerView.snp.remakeConstraints { (make) -> Void in
        make.width.top.equalToSuperView()
        make.top.equalTo(self.view.safeArea.top)
        make.bottom.equalTo(self.view.safeArea.bottom)
  }




extension UIView {
    var safeArea: ConstraintBasicAttributesDSL {
        if #available(iOS 11.0, *) {
            return self.safeAreaLayoutGuide.snp
        }
        return self.snp
    }


    var isIphoneX: Bool {

        if #available(iOS 11.0, *) {
            if topSafeAreaInset > CGFloat(0) {
                return true
            } else {
                return false
            }
        } else {
            return false
        }

    }

    var topSafeAreaInset: CGFloat {
        let window = UIApplication.shared.keyWindow
        var topPadding: CGFloat = 0
        if #available(iOS 11.0, *) {
            topPadding = window?.safeAreaInsets.top ?? 0
        }

        return topPadding
    }

    var bottomSafeAreaInset: CGFloat {
        let window = UIApplication.shared.keyWindow
        var bottomPadding: CGFloat = 0
        if #available(iOS 11.0, *) {
            bottomPadding = window?.safeAreaInsets.bottom ?? 0
        }

        return bottomPadding
    }
}
johndpope
  • 5,035
  • 2
  • 41
  • 43
0

For those of you who change to landscape mode, you gotta make sure to use viewSafeAreaInsetsDidChange after the rotation to get the most updated values:

private var safeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)

override func viewSafeAreaInsetsDidChange() {
        if #available(iOS 11.0, *) {
            safeAreaInsets = UIApplication.shared.keyWindow!.safeAreaInsets
        }
}
Oz Shabat
  • 1,434
  • 17
  • 16
0

Swift 4

if let window = UIApplication.shared.windows.first {
    
    let topPadding = window.safeAreaInsets.top
    let bottomPadding = window.safeAreaInsets.bottom
    
}

Use from class

class fitToTopInsetConstraint: NSLayoutConstraint {
    
    override func awakeFromNib() {
        
        if let window = UIApplication.shared.windows.first {
            
            let topPadding = window.safeAreaInsets.top
            
            self.constant += topPadding
            
        }
        
    }
}

class fitToBottomInsetConstraint: NSLayoutConstraint {
    
    override func awakeFromNib() {
        
        if let window = UIApplication.shared.windows.first {
            
            let bottomPadding = window.safeAreaInsets.bottom
            
            self.constant += bottomPadding
            
        }
        
    }
}

enter image description here

enter image description here

You will see safe area padding when you build your application.

0
extension UIViewController {
    var topbarHeight: CGFloat {
        return
            (view.window?.safeAreaInsets.top ?? 0) +
            (view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0.0) +
            (self.navigationController?.navigationBar.frame.height ?? 0.0)
    }
}
Serg Smyk
  • 613
  • 5
  • 11
0

Here's a free function based on other answers that should be callable once your rootController is layed out from anywhere. You can use it as a free standing function.

    func safeAreaInsets() -> UIEdgeInsets? {
    (UIApplication
        .shared
        .keyWindow?
        .rootViewController)
        .flatMap {
            if #available(iOS 11.0, *) {
                return $0.view.safeAreaInsets
            } else {
                return .init(
                    top: $0.topLayoutGuide.length,
                    left: .zero,
                    bottom: $0.bottomLayoutGuide.length,
                    right: .zero
                )
            }
        }
}
SmileBot
  • 19,393
  • 7
  • 65
  • 62
0

For iPhone 14 devices use like following.

let app = UIApplication.shared
var statusBarHeight: CGFloat = 0.0
let window = app.windows.filter {$0.isKeyWindow}.first
statusBarHeight = window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
let topPadding = window?.safeAreaInsets.top ?? 0.0
statusBarHeight = statusBarHeight >= topPadding ? statusBarHeight:topPadding
Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
Chetu
  • 428
  • 1
  • 7
  • 19
-1

Here is a simple answer to find safe area height for all iphone

let window = UIApplication.shared.windows[0]

let SafeAreaHeight = window.safeAreaLayoutGuide.layoutFrame.size.height
Slava Rozhnev
  • 9,510
  • 6
  • 23
  • 39
-1

Nice and simple

    struct ContentView: View {
            var body: some View {
        
                let safeAreaInsets = UIApplication.shared.windows.first?.safeAreaInsets ?? .zero
                let safeAreaTop = safeAreaInsets.top
                let safeAreaTop = safeAreaInsets.bottom
           }
     }
Some guy
  • 51
  • 9