ChatGPT解决这个技术问题 Extra ChatGPT

Get safe area inset top and bottom heights

On the new iPhone X, what would be the most proper way to get both top and bottom height for the unsafe areas?

https://i.stack.imgur.com/hQXf7.png


K
Khuong

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 and above

// 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
}

@KrutikaSonawala bottomPadding + 10( your value)
This doesn't seem to work for me - printing out the value of UIApplication.shared.keyWindow?.safeAreaInsets returns all 0s. Any ideas?
I did, I can constrain against safeAreaLayoutGuides of any view but directly printing out the value of the key window's safeAreaInsets just returns a zero rect. This is problematic because I need to manually constrain a view that has been added to the key window but is not in the root view controller's view hierarhcy.
For me, keyWindow was always nil so I changed it to windows[0] and removed the ? from the optional chaining, then it worked.
It is working only when a controller's view has already appeared.
K
Krishna Raj Salim

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

https://i.stack.imgur.com/oe5xe.png


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?
Once views have been laid out this will give you the correct answer
Actually @user6788419's answer looks nice too and way shorter.
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.
@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" developer.apple.com/documentation/uikit/uiview/…
f
fake girlfriends

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
}

Please declare using let topSafeArea: CGFloat instead!
avoid using viewDidLayoutSubviews (which can be called multiple times), here's a solution which will work even in viewDidLoad: stackoverflow.com/a/53864017/7767664
@user924 then the values won’t be updated when the safe areas change (i.e. the double-height status bar)
@bsod then this will be better stackoverflow.com/a/3420550/7767664 (because it's only for status bar and not other views)
Or instead of going through the app delegate you can just use the safe area API Apple built into the view controller.
S
ScottyBlades

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
  }

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.
best answer so far
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.
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
m
mojtaba al moussawi

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:

https://i.stack.imgur.com/VRzXq.png

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.

https://i.stack.imgur.com/pRXS0.png

https://i.stack.imgur.com/GwfzV.png

Now, if navigation bar added

https://i.stack.imgur.com/9XljA.png

https://i.stack.imgur.com/B4kDX.png

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


u
user1039695

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 })
    }
}

M
Martin

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
}

Perfect answer 🤙🏻
m
malhal

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

T
Theodore

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.


M
Michael

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.


T
Tai Le

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
}

P
Peter
UIWindow *window = [[[UIApplication sharedApplication] delegate] window]; 
CGFloat fBottomPadding = window.safeAreaInsets.bottom;

L
Lorenzo Fiamingo

For SwiftUI:

Code

private struct SafeAreaInsetsKey: EnvironmentKey {
    static var defaultValue: EdgeInsets {
        UIApplication.shared.windows[0].safeAreaInsets.insets
    }
}

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)
    }
}

I
Ilya

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.


D
DmitryShapkin

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


J
Japes

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
    }

j
johndpope

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
    }
}

O
Oz Shabat

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
        }
}

S
Saharat Sittipanya

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
            
        }
        
    }
}

https://i.stack.imgur.com/AnVAx.png

https://i.stack.imgur.com/YbBVq.png

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


S
Serg Smyk
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)
    }
}

S
SmileBot

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
                )
            }
        }
}

S
Slava Rozhnev

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

This is the same solution as in this other answer.