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.
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
iPhone 8 Interface Builder Aligned to Safe Area
iPhone X Simulator - Master Screen
iPhone X Simulator - Details Screen
iPhone 8 Simulator - Master Screen
iPhone 8 Simulator - Details Screen
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
}
}