iPhone X Aligning object bottom to Safe Area ruins look on other devices

Another way to achieve this directly from the storyboard is to create two constraints:
1. Between your element and safe area with 250 priority with 0 constant
2. Between your element and the superview bottom with 750 priority and 20 constant and greater Than or Equal relation.

enter image description here


The Apple Docs states there is a new declaration in iOS 11 which can be a solution to this problem. Currently iPhone X and iPhone 8 share the same size class so we must come up with another solution.

var additionalSafeAreaInsets: UIEdgeInsets { get set }

Add the following code below in your AppDelegate and all children of the rootViewController will inherit the additional safe area. Example screenshots below describe this behavior.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.

    if !self.isIphoneX() {
        self.window?.rootViewController?.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0)
    }

    return true
}

func isIphoneX() -> Bool {
    if #available(iOS 11.0, *) {
        if ((self.window?.safeAreaInsets.top)! > CGFloat(0.0)) {
            return true;
        }
    }
    return false
}

iPhone X Interface Builder Aligned to Safe Area

enter image description here

iPhone 8 Interface Builder Aligned to Safe Area

enter image description here

iPhone X Simulator - Master Screen

enter image description here

iPhone X Simulator - Details Screen

enter image description here

iPhone 8 Simulator - Master Screen

enter image description here

iPhone 8 Simulator - Details Screen

enter image description here


After spending a fair bit of time attempting to fix this type of issue, originally using Marcos's solution, I ran into a case where it didn't solve it - specifically in the case where the "safe area" being non-zero height but not being the screen's safe area meant the offset was 0 instead of the minimum 20. Example being an arbitrary view controller with a bottom safe area set via the additionalSafeAreaInsets.

The solution was to check whether our view is aligned with the window with a non zero safe area when the safe area insets change, and adjust the constraint's bottom offset to the safe area based on that. The following leads to a 20pt offset from the bottom in the rectangular style screen, and 0 in a full-screen with safe area style screen (iPhone X, latest iPad Pro, iPad slide overs etc).

// In UIView subclass, or UIViewController using viewSafeAreaInsetsDidChange instead of safeAreaInsetsDidChange

@available(iOS 11.0, *)
override func safeAreaInsetsDidChange() {
    super.safeAreaInsetsDidChange()
    isTakingCareOfWindowSafeArea = self.isWithinNonZeroWindowBottomSafeArea
}

private var isTakingCareOfWindowSafeArea = false {
    didSet {
        guard isTakingCareOfWindowSafeArea != oldValue else { return }
        // Different offset based on whether we care about the safe area or not
        let offset: CGFloat = isTakingCareOfWindowSafeArea ? 0 : 20
        // bottomConstraint is a required bottom constraint to the safe area of the view.
        bottomConstraint.constant = -offset
    }
}
extension UIView {

    /// Allows you to check whether the view is dealing directly with the window's safe area. The reason it's the window rather than
    /// the screen is that on iPad, slide over apps and such also have this nonzero safe area. Basically anything that doesn't have a square area (such as the original iPhones with rectangular screens).
    @available(iOS 11.0, *)
    var isWithinNonZeroWindowBottomSafeArea: Bool {

        let view = self

        // Bail if we're not in a window
        guard let window = view.window else { return false }
        let windowBottomSafeAreaInset = window.safeAreaInsets.bottom

        // Bail if our window doesn't have bottom safe area insets
        guard windowBottomSafeAreaInset > 0 else { return false }

        // Bail if our bottom area doesn't match the window's bottom - something else is taking care of that
        guard windowBottomSafeAreaInset == view.safeAreaInsets.bottom else { return false }

        // Convert our bounds to the window to get our frame within the window
        let viewFrameInWindow = view.convert(view.bounds, to: window)

        // true if our bottom is aligned with the window
        // Note: Could add extra logic here, such as a leeway or something
        let isMatchingBottomFrame = viewFrameInWindow.maxY == window.frame.maxY

        return isMatchingBottomFrame

    }

}