66

Is enabling Safe Area Layout Guides compatible to iOS below 11?

enter image description here

Ted
  • 22,696
  • 11
  • 95
  • 109
  • 6
    The first Google hit for "safe area layout guide" is https://useyourloaf.com/blog/safe-area-layout-guide/, where it is claimed that *"... you can switch to using the safe area layout guide in Interface Builder even if you still target iOS 10 and older."* – Martin R Sep 12 '17 at 19:52
  • 3
    Is there a special reason to tag the question with [iphone-x] ? – Martin R Sep 12 '17 at 19:55
  • 2
    I think I know why, on iPhoneX, a tabBar will screw up without safeareaInsets – Yuji Sep 12 '17 at 22:32
  • 4
    If your Deployment Target is set to anything below iOS 9.0, Xcode will give you an error that the Safe Area Layout Guides aren't supported. – rmaddy Sep 13 '17 at 14:32
  • 3
    Is there any was to use support safe area and support iOS 8? im thinking to to add programatically to all screens.. – Pushparaj Oct 05 '17 at 11:29
  • 2
    Unfortunately, even if apple says that Safe Area is backwards compatible on Storyboard, it really isn't and will mess up your layout on ios 9 and 10 devices. There's a radr bug for this. Basically with safe area turned on there will be a 20 pts blank space at the top of all your views on ios 9 and 10. – Anjan Biswas Nov 13 '17 at 00:30

16 Answers16

56

I managed to work with the new Safe Area layout guides and maintain backwards compatibility with iOS 9 and iOS 10: (EDIT: as pointed out in the comments by @NickEntin, this implementation will presume there is a status bar present, which won't be true in landscape on the iPhone X. Resulting in to much space to the top (20 points). It will run perfectly fine however.

E.g. if you want a view to be 10 points below the status bar (and 10 points below the sensor housing on iPhone X):

  1. In your XIB, go to File Inspector and enable the safe are by checking Use Safe Area Layout Guides.
  2. Create a constraint from the view's top to the main view's top, with >= (greater than or equal) constraint, constant 30 (30 because we want 10 points spacing to the status bar which is 20 points high) and priority High (750).
  3. Create a constraint from the view's top to the Safe Area's top, with = (equal) constraint, constant 10 and priority Low (250).

The same can be done for a view at the bottom (and for leading/trailing or left/right to the Safe Area):

  1. In your XIB, go to File Inspector and enable the safe are by checking Use Safe Area Layout Guides.
  2. Create a constraint from the view's bottom to the main view's bottom, with >= (greater than or equal) constraint, constant 10 and priority High (750).
  3. Create a constraint from the view's bottom to the Safe Area's bottom, with = (equal) constraint, constant 10 and priority Low (250).
thijsonline
  • 1,048
  • 11
  • 15
  • 1
    Interesting, have you noticed, that on iPhoneX there is no status bar in landscape mode even if App wants to show it? If my observation is true, then higher priority constraint to make distance covering status bar will override 0 for the safe area and this will look not so good on iPhoneX – Nick Entin Dec 19 '17 at 14:36
  • 1
    @NickEntin thanks for noticing. AFAIK the status bar is indeed always hidden in landscape mode on iPhone X, and this implementation does not handle this (acting like the status bar is actually there). – thijsonline Dec 20 '17 at 09:31
  • 2
    Safe layout is main view in xcode 9. How are we going to apply point 2? Here in 2 and 3, wouldn't be we creating constraints with same view? – NightFury Jan 02 '18 at 09:55
48

The backwards compatibility of Safe Areas for iOS 9 & iOS 10 only works if you are using storyboards. If you are using xibs, there is no layout guide to fall back to. https://forums.developer.apple.com/thread/87329

The workarounds seem to be either

(a) migrate your xibs into storyboards, or

(b) add some additional constraints programmatically.

If (a) is not really an option, the manual approach will be something like this:

Assuming you have a view in your xib that you want to keep within the safe area (i.e. below any status bar or navigation bar).

  1. Add constraints in your xib between your view and the safe area for iOS 11. Assign the top constraint to a priority of 750.

    add a top constraint

  2. In your view controller, add a property:

    @property (nonatomic, strong) NSLayoutConstraint *topLayoutConstraint;
    

    And then in viewDidLayoutSubviews:

    - (void)viewDidLayoutSubviews {
        [super viewDidLayoutSubviews];
    
        if (@available(iOS 11, *)) {
            // safe area constraints already set
        }
        else {
            if (!self.topLayoutConstraint) {
                self.topLayoutConstraint = [self.<yourview>.topAnchor constraintEqualToAnchor:self.topLayoutGuide.bottomAnchor];
                [self.topLayoutConstraint setActive:YES];
            }
        }
    }
    

    The new constraint will only be created for iOS 9 & iOS 10, has a default priority of 1000, and overrides the one in the xib.

  3. Repeat for a bottom constraint if you need to avoid the home indicator.

Swift 4 version:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if #available(iOS 11, *) {
        // safe area constraints already set
    } else {
        if topLayoutConstraint == nil {
            topLayoutConstraint = <yourview>.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor)
            topLayoutConstraint?.isActive = true
        }
    }
}
Politta
  • 400
  • 4
  • 10
Dobster
  • 560
  • 4
  • 8
  • 1
    @RawMean `#available(iOS 11, *)` will check for iOS 11 and newer – Politta Mar 04 '18 at 22:35
  • Thanks for the great explanation! Inspired by your ideas I created a subclass of `NSLayoutConstraint` which makes it more convenient to use because nothing has to be implemented in each viewcontroller where you might use that xib. it's at the very end of this post: https://stackoverflow.com/a/52145680/4181169 – iVentis Sep 03 '18 at 08:15
  • thanks. Why did you put this into `viewDidLayoutSubviews` instead of `viewDidLoad`? – user2378197 Jan 09 '19 at 17:26
  • @user2378197 Because there might be layout code for in `layoutSubviews` that should go first. I think `viewDidLoad` is also OK depending on what you are doing. – Dobster Feb 03 '19 at 04:49
  • REiterating user2378197's comment - I put this in viewDidLoad with no reference to it and it works fine. – zenchemical Mar 21 '19 at 23:38
19

There's definitely at least one backwards compatibility issue with iOS 11's safe area constraints that I've observed in the Xcode 9 GM--pushed view controllers with safe area constraints.

If your navigation bar is hidden and you push a safe area top-constrained view, the pushed view will overlap the status bar on iOS 9 & 10.

If the navigation bar is visible, and "under top bars" disabled, the pushed view will still slide up under the nav bar to get to the top of the screen. The navigation bar is placed correctly.

On iOS 11, the layout will be correct in both cases.

Here's a simple example: http://www.filedropper.com/foobar

And here's a video of it w/nav bar hidden (iOS 10.3 on left, iOS 11 on right): https://vimeo.com/234174841/1e27a96a87

Here's a version where the nav bar is visible (enabled in the nib): https://vimeo.com/234316256/f022132d57

I filed this as Radar #34477706.

Thanks to @Sander for pointing out the nav bar visible case.

clarus
  • 2,455
  • 18
  • 19
  • 2
    If navigation bar isn't hidden the bug is the same. On versions iOS 9 and 10 pushed view is laid under the navigation bar. Or this case should be handled in different way? – Sander Sep 18 '17 at 11:53
  • 3
    So, in terms of workarounds, a few things come to mind, none of which are great, but... 1) You could have separate nibs/storyboards for pre-iOS 11. Depending on the complexity of your app, that could be a real pain or super simple. or 2) If the nav bar is hidden, having a top constraint that's >= 20 to superview for views that have a status bar but no nav (at 1000 priority) as well as the safe area one (at 750 priority). That works and shouldn't cause problems going forward. For the navbar visible case, that's more tricky. I'm still working on a good solution for that. – clarus Sep 18 '17 at 13:27
  • Hi @clarus, thanks for the information! Did you get any response from Apple? – ernewston Sep 28 '17 at 14:56
  • Nothing yet, but typically they won’t get back until they fix issues and need them validated. That could take months. – clarus Sep 29 '17 at 14:32
  • right, status bar height is ignored on non-ios11. How to have correct layout both in ios11 and pre-ios11 ? – Isaak Osipovich Dunayevsky Oct 05 '17 at 10:08
  • Is this still broken in iOS 11.1? – mpoisot Nov 02 '17 at 19:53
  • 3
    Yes it is. It's ridiculous. I had to use many workarounds to get things to play well in iOS 9 and 10. It's Apple's way of saying "yeah, sorry to hear that..... but we don't care about ios10 or 9". :) – Anjan Biswas Nov 11 '17 at 16:33
7

Yes, your project/app will work in iOS versions prior to iOS 11 without any issue. In iOS versions prior to 11, it replaces/considers Safe Area Layout into normal AutoLayout and follows Rules of Top and Bottom layout guide.

I tested my existing project with and without 'SafeAreaLayout' on both platforms (iOS 11 and backward iOS 10). It's working fine.

Just make sure:

  • If you have designed your project/User Interface in AutoLayout; constraints of your UIElement follows/relative to Top and Bottom layout guide (not to superview). So by a single click (enable) on SafeAreaLayout option, will automatically implement SafeArea layout properly for all Interface Builders files in your storyboard.

  • If you have designed your project/User Interface in SafeAreaLayout; then it will automatically follow Top and Bottom layout guide in backward iOS.

Here is sample snapshot with result, By enabling or disabling Safe Area layout, won't effect on existing design.

Safe Area Layout: enter image description here

AutoLayout

enter image description here

In short, an answer to your question is: "Enabling Safe Area Layout Guides compatible to iOS prior to 11"

You can implement Safe Area Layout in your project/app and it will work fine with previous iOS versions by converting Safe Area Layout into Top and Bottom Layout.

patridge
  • 26,385
  • 18
  • 89
  • 135
Krunal
  • 77,632
  • 48
  • 245
  • 261
  • 1
    strangely, the safe area guide does not work for me in iOS 10... it just assumes a value of 0 and the content overlaps the status bar. In iOS 11 it works fine. Any idea why? – Tiago Lira Sep 14 '17 at 16:31
  • 1
    If you are working with Xcode 9 then your safe area layout automatically converts safe area into Top and Bottom layout for iOS 10 and backwards.. Can you share a snapshot, for your storyboard layout? – Krunal Sep 14 '17 at 17:31
  • 15
    What I'm finding is that it converts it well in storyboards, but fails in xib files... it simply ignores the status bar size in iOS 10 – Tiago Lira Sep 14 '17 at 17:40
  • 3
    In my tests it works fine for iOS 11, fails on iOS 10. Seems like this stuff still has some bugs. I'll post a full question later, thanks for the help. – Tiago Lira Sep 14 '17 at 18:02
  • Not true if using navigation bar and/or tab bar. – Jonny Apr 19 '18 at 04:59
7

If you use xibs without storyboard then they don't have layout guides on ios 10. So move xib to storyboard to have backward compatibility.

ihar
  • 253
  • 3
  • 9
7

Swift 5

I just do this. It is simple and very close to the real thing (just added an 'r').

extension UIView {
    var saferAreaLayoutGuide: UILayoutGuide {
        get {
            if #available(iOS 11.0, *) {
                return self.safeAreaLayoutGuide
            } else {
                return self.layoutMarginsGuide
            }
        }
    }
}

Use like this:

button.topAnchor.constraint(equalTo: view.saferAreaLayoutGuide.topAnchor, constant: 16)
Argus
  • 2,241
  • 1
  • 22
  • 27
Mads Buus
  • 488
  • 5
  • 5
6

I was using this in Objective-C when I had a Navigation Bar in my UIView and had good results for iOS 10. In case you use SafeArea in your xib then you may add in your viewDidLoad:

if (@available(iOS 11.0, *)) {}
else {
    self.edgesForExtendedLayout = UIRectEdgeNone;
}
Dmitry A.
  • 588
  • 8
  • 13
  • Helped hugely with that extra margin at the top of the view equal to the layout normally extended under the navigation bar which just sits in the visible area on iOS 9 & 10. – LordParsley May 23 '18 at 10:24
  • Yeah, you're right. This position fix only works with navigation bar. It's not works without navigation bar. – Johnny Dec 06 '18 at 19:58
4

for iOS 9:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    self.navigationController.navigationBar.translucent = NO;
}

if you enable autolayout and add view constraints to the safe area, you are good to go above iOS 11+ , but it may not work well with iOS 9 and your view may appear under your navigation bar. To solve this problem, you can disable translucent attribute in 'viewWillAppear:(BOOL)animated' method.

To not to break previous state of translucent attribute of your navigation bar, you should keep previous value and re-set it in 'viewWillDisappear:(BOOL)animated'

@interface YourViewController ()
@property (nonatomic, assign) BOOL translucentState;
@end

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    self.translucentState = self.navigationController.navigationBar.translucent;
    self.navigationController.navigationBar.translucent = NO;
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    self.navigationController.navigationBar.translucent = self.translucentState;
}

P.S. should not use edgesForExtendedLayout with doing this:

self.edgesForExtendedLayout = UIRectEdgeNone;

Check out Apple documentation: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621515-edgesforextendedlayout

Fatih Aksu
  • 3,801
  • 2
  • 21
  • 20
3

In Objective-C for top and bottom margin when on iPhone-X

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

    NSLayoutConstraint *bottomConstraint   = [NSLayoutConstraint constraintWithItem:self.childView
                                                                              attribute:NSLayoutAttributeBottom
                                                                              relatedBy:NSLayoutRelationEqual
                                                                                 toItem:self.parentView.safeAreaLayoutGuide
                                                                              attribute:NSLayoutAttributeBottom
                                                                             multiplier:1.0
                                                                               constant:0];


    NSLayoutConstraint *topConstraint   = [NSLayoutConstraint constraintWithItem:self.childView
                                                                       attribute:NSLayoutAttributeTop
                                                                       relatedBy:NSLayoutRelationEqual
                                                                          toItem:self.parentView.safeAreaLayoutGuide
                                                                       attribute:NSLayoutAttributeTop
                                                                      multiplier:1.0
                                                                        constant:0];


} else {

    NSLayoutConstraint *bottomConstraint   = [NSLayoutConstraint constraintWithItem:self.childView
                                                                          attribute:NSLayoutAttributeBottom
                                                                          relatedBy:NSLayoutRelationEqual
                                                                             toItem:self.parentView
                                                                          attribute:NSLayoutAttributeBottom
                                                                         multiplier:1.0
                                                                           constant:0];


    NSLayoutConstraint *topConstraint   = [NSLayoutConstraint constraintWithItem:self.childView
                                                                       attribute:NSLayoutAttributeTop
                                                                       relatedBy:NSLayoutRelationEqual
                                                                          toItem:self.parentView
                                                                       attribute:NSLayoutAttributeTop
                                                                      multiplier:1.0
                                                                        constant:0];

}
Chandan
  • 500
  • 3
  • 10
3

"safe area layout guide" is backward compatible. Well, unless you use it in xib. With storyboard it seems ok.

I solved my problem by accessing the "Top layout constraint" from the first object at the top of my view.

@property (weak, nonatomic) IBOutlet NSLayoutConstraint *topLayoutConstraint;

then, I changed the Constant value to that constraint and refresh the view. For example, if you use a navigation bar (44 height) plus the status bar (20 height) :

if (SYSTEM_VERSION_LESS_THAN(@"11.0")) {
    _topLayoutConstraint.constant = 64;
    [self.view layoutIfNeeded];
}

With SYSTEM_VERSION_LESS_THAN which is defined like that :

#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
gduh
  • 1,079
  • 12
  • 30
3

I found a more convenient way where you only need to subclass the NSLayoutConstraintthat is pinned to your safeArea.

It's kinda hacky since you have to get the ViewController from a UIView but in my opinion, that's an easy and good alternative until Apple finally fixes backward compatibility for the safeArea in Xibs.

Subclass:

class SafeAreaBackwardsCompatabilityConstraint: NSLayoutConstraint {
private weak var newConstraint: NSLayoutConstraint?

override var secondItem: AnyObject? {
    get {
        if #available(iOS 11.0, *) {}
        else {
            if let vc = (super.secondItem as? UIView)?.parentViewController, newConstraint == nil {
                newConstraint = (self.firstItem as? UIView)?.topAnchor.constraint(equalTo: vc.topLayoutGuide.bottomAnchor)
                newConstraint?.isActive = true
                newConstraint?.constant = self.constant
            }
        }
        return super.secondItem
    }
}

override var priority: UILayoutPriority {
    get {
        if #available(iOS 11.0, *) { return super.priority }
        else { return 750 }
    }
    set { super.priority = newValue }
}
}

private extension UIView {
var parentViewController: UIViewController? {
    var parentResponder: UIResponder? = self
    while parentResponder != nil {
        parentResponder = parentResponder!.next
        if let viewController = parentResponder as? UIViewController {
            return viewController
        }
    }
    return nil
}
}

Xib:

enter image description here

iVentis
  • 993
  • 6
  • 19
2

I have backward compatibility issues with WKWebView & Safe Area on iOS 9. By some reason, WKWebView simply ignores safe area layout settings.

Nik
  • 9,063
  • 7
  • 66
  • 81
  • Are you pushing it, presenting it or is it the root view of your app? – clarus Sep 18 '17 at 13:33
  • I got it fixed by moving view frame init from viewDidLoad to viewWillAppear. Quite obvious, but it wasn't necessary for iOS 10, iOS 11. New iOS versions pick up correct sizing after the view is initialized. – Nik Sep 18 '17 at 15:15
2

Here's what I did with my Projects

In my case both my topConstraint and bottomConstraints are @IBOutlets. This is also compatible for iOS 8.

My initial configuration for the top and bottom constraints are for the normal iPhones, which is why I'm only editing the constraints for iPhone X

    // iOS 11 Layout Fix. (For iPhone X)
    if #available(iOS 11, *) {
        self.topConstraint.constant = self.topConstraint.constant + self.view.safeAreaInsets.top

        self.bottomConstraint.constant = self.bottomConstraint.constant + self.view.safeAreaInsets.bottom
    }

.

NOTE: self.view is your superView which is why I'm using it for safeAreaInsets

Zonily Jame
  • 5,053
  • 3
  • 30
  • 56
2

When you have a generic ViewController that all your ViewControllers extend, another solution would be to put the items that should be adjusted in an IBOutletCollection and adjust them programmatically in that GenericViewController. Here's my code :

@IBOutlet var adjustTopSpaceViews: [UIView]?

override func viewDidLoad() {
    super.viewDidLoad()
    adjustViews()
    ....
}

func adjustViews() {
    guard let views = adjustTopSpaceViews,
        ProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11 else {
            return
    }
    let statusBarHeight = UIApplication.shared.statusBarFrame.height
    for subview in views {
        subview.superview?.constraints.filter({ (constraint) -> Bool in
            return constraint.firstAttribute == .top
                && constraint.secondAttribute == .top
                && (constraint.firstItem as? UIView == subview || constraint.secondItem as? UIView == subview)
        }).forEach({ (constraint) in
            constraint.constant += (constraint.firstItem as? UIView == subview) ? statusBarHeight : -statusBarHeight
        })
    }
}
1

Here is my iOS 9 to iOS 11+ solution wrapper in swift 4+

    let safeAreaTopAnchor:NSLayoutYAxisAnchor?
    if #available(iOS 11.0, *) {
        safeAreaTopAnchor = contentView.safeAreaLayoutGuide.topAnchor
    } else {
        // Fallback on earlier versions

        var parentViewController: UIViewController? {
            var parentVCResponder: UIResponder? = self
            while parentVCResponder != nil {
                parentVCResponder = parentVCResponder!.next
                if let viewController = parentVCResponder as? UIViewController {
                    return viewController
                }
            }
            return nil
        }

        safeAreaTopAnchor = parentViewController?.topLayoutGuide.bottomAnchor

    }
Rafat touqir Rafsun
  • 2,777
  • 28
  • 24
0

Simple Swift 4 Solution:

First set to top constraint priority to the safe area to 750, then:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    if #available(iOS 11, *) {
        // Safe area constraints already set.
    } else {
        NSLayoutConstraint.activate([
            self.yourView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor)
        ])
    }
}
ThiagoAM
  • 1,432
  • 13
  • 20