68

I started adapting my app for iPhone X and found an issue in Interface Builder. The safe area layout guides are supposed to be backwards compatible, according to official Apple videos. I found that it works just fine in storyboards.

But in my XIB files, the safe area layout guides are not respected in iOS 10.

They work fine for the new OS version, but the iOS 10 devices seem to simply assume the safe area distance as zero (ignoring the status bar size).

Am I missing any required configuration? Is it an Xcode bug, and if so, any known workarounds?

Here is a screenshot of the issue in a test project (left iOS 10, right iOS 11):

safe area alignment on top of the screen

Tiago Lira
  • 2,543
  • 1
  • 16
  • 16

9 Answers9

71

There are some issues with safe area layout and backwards compatibility. See my comment over here.

You might be able to work around the issues with additional constraints like a 1000 priority >= 20.0 to superview.top and a 750 priority == safearea.top. If you always show a status bar, that should fix things.

A better approach may be to have separate storyboards/xibs for pre-iOS 11 and iOS-11 and up, especially if you run into more issues than this. The reason that's preferable is because pre-iOS 11 you should layout constraints to the top/bottom layout guides, but for iOS 11 you should lay them out to safe areas. Layout guides are gone. Laying out to layout guides for pre-iOS 11 is stylistically better than just offsetting by a min of 20 pixels, even though the results will be the same IFF you always show a status bar.

If you take this approach, you'll need to set each file to the correct deployment target that it will be used on (iOS 11, or something earlier) so that Xcode doesn't give you warnings and allows you to use layout guides or safe areas, depending. In your code, check for iOS 11 at runtime and then load the appropriate storyboard/xibs.

The downside of this approach is maintenance, (you'll have two sets of your view controllers to maintain and keep in sync), but once your app only supports iOS 11+ or Apple fixes the backward compatibility layout guide constraint generation, you can get rid of the pre-iOS 11 versions.

By the way, how are you displaying the controller that you're seeing this with? Is it just the root view controller or did you present it, or..? The issue I noticed has to do with pushing view controllers, so you may be hitting a different case.

clarus
  • 2,405
  • 16
  • 19
  • My specific problem is only on XIB files, and seems to happen for both pushed and presented controllers. Your workaround with multiple constraints sounds like a good fix for most situations. Thanks! I'm still hoping they will solve these issues before the iPhoneX arrives. – Tiago Lira Sep 21 '17 at 11:34
  • Very thank for the priority suggestion ! Now works like a charm on iOS 9 and iOS 11 under iPhone X. – alex.bour Sep 21 '17 at 19:06
  • May I comment ask on this "If you always show a status bar, that should fix things."? I can't see the way to show status bar on iPhone X simulator in landscape. Then the described workaround of "1000 priority >= 20.0 to superview.top and a 750 priority == safearea.top" would have a 20px gap on top for the iPhone X in landscape. My solution was to remove the constraint of "1000 priority >= 20.0" in the viewDidLoad if iOS < 11. Am I missing the way to force status bar in iPhone X landscape? – Stanislav Dvoychenko Sep 27 '17 at 21:30
  • 2
    That’s a good point. I was only dealing with portrait for the app i was working with. For iOS 11, theres no reason to have that constraint anyway. That sounds like a good solution, or programmatically adding a top layout guide constraint for < ios 11 and not always having that >= 20 one. – clarus Sep 27 '17 at 21:37
  • @clarus, Good point as well. Probably depends on how explicit/implicit one would need this handling to be. In the end, I decided to keep the xib with Safe area, "1000 priority >= 20.0" in the xib and removing the constraint in (if (@available(iOS 11.0, *))). I have only 2 of such xibs, used for a lot of controllers (VC inheritance is why I can't move them to storyboards simply). In case on these 2, I probably will prefer to keep everything very explicit in both xibs and code. Should I have tens of these, I'd probably go with your suggested order, future maintenance wise. – Stanislav Dvoychenko Sep 28 '17 at 09:32
  • The priority setup you mentioned is the best thing I've seen all week. It was driving me nuts. Thank you. – AnBisw Nov 10 '17 at 01:11
  • I think having seperate storyboards is not a good solution. I'd rather write more code to avoid this – C0D3 Feb 02 '18 at 21:41
  • 1
    Instead of giving double constraint Apple should provide backward compatibility for **ios<11** @clarus or it might include in wwdc 2018 ? – Jack May 29 '18 at 05:13
  • Great solution, thanks! One question though, when adding the superview.top constraint, should it be relative to margins or not? – jowie Jun 29 '18 at 08:29
14

Currently, backward compatibility doesn't work well.

My solution is to create 2 constraints in interface builder and remove one depending on the ios version you are using:

  • for ios 11: view.top == safe area.top
  • for earlier versions: view.top == superview.top + 20

Add them both as outlets as myConstraintSAFEAREA and myConstraintSUPERVIEW respectively. Then:

override func viewDidLoad() {
    if #available(iOS 11.0, *) {
        view.removeConstraint(myConstraintSUPERVIEW)
    } else {
        view.removeConstraint(myConstraintSAFEAREA)
    }
}
Martin Massera
  • 1,307
  • 18
  • 33
  • 1
    This is a much more maintainable/cleaner approach than the accepted answer, imo. Note this does depend on having a status bar always – Jesse Naugher Apr 16 '18 at 21:58
  • 1
    I agree - have been looking around for answers and this seems way cleaner than having separate XIB files for iOS 10 and iOS 11. – Brian Sachetta Jul 31 '18 at 18:42
6

For me, a simple fix for getting it to work on both versions was

    if #available(iOS 11, *) {}
    else {
        self.edgesForExtendedLayout = []
    }

From the documentation: "In iOS 10 and earlier, use this property to report which edges of your view controller extend underneath navigation bars or other system-provided views. ". So setting them to an empty array makes sure the view controller does not extend underneath nav bars.

Docu is available here

Falco Winkler
  • 786
  • 1
  • 12
  • 22
  • 2
    Unfortunately this only works if the navigation bar is visible. If the nav bar is hidden, the view will still extend under the status bar with `edgesForExtendedLayout = []` – OliverD Jul 08 '19 at 09:33
3

I have combined some of the answers from this page into this, which works like a charm (only for top layout guide, as requested in the question):

  1. Make sure to use safe area in your storyboard or xib file
  2. Constraint your views to the safe areas
  3. For each view which has a constraint attached to the SafeArea.top
    • Create an IBOutlet for the view
    • Create an IBOutler for the constraint
  4. Inside the ViewController on viewDidLoad:

    if (@available(iOS 11.0, *)) {}
    else {
        // For each view and constraint do:
        [self.view.topAnchor constraintEqualToAnchor:self.topLayoutGuide.bottomAnchor].active = YES;
        self.constraint.active = NO;
    }
    

Edit:

Here is the improved version I ended up using in our codebase. Simply copy/paste the code below and connect each view and constraints to their IBOutletCollection.

@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *constraintsAttachedToSafeAreaTop;
@property (strong, nonatomic) IBOutletCollection(UIView) NSArray *viewsAttachedToSafeAreaTop;


if (@available(iOS 11.0, *)) {}
else {
    for (UIView *viewAttachedToSafeAreaTop in self.viewsAttachedToSafeAreaTop) {
        [viewAttachedToSafeAreaTop.topAnchor constraintEqualToAnchor:self.topLayoutGuide.bottomAnchor].active = YES;
    }
    for (NSLayoutConstraint *constraintAttachedToSafeAreaTop in self.constraintsAttachedToSafeAreaTop) {
        constraintAttachedToSafeAreaTop.active = NO;
    }
}

The count of each IBOutletCollection should be equal. e.g. for each view there should be its associated constraint

Tumata
  • 1,237
  • 10
  • 14
0

I ended up deleting the constraint to safe area which I had in my xib file. Instead I made an outlet to the UIView in question, and from code I hooked it up like this, in viewDidLayoutSubviews.

let constraint = alert.viewContents.topAnchor.constraint(equalTo: self.topLayoutGuide.bottomAnchor, constant: 0)
constraint.priority = 998
constraint.isActive = true

This ties a small "alert" to top of screen but makes sure that the contents view within the alert is always below the top safe area(iOS11ish)/topLayoutGuide(iOS10ish)

Simple and a one-off solution. If something breaks, I'll be back .

Jonny
  • 14,972
  • 14
  • 101
  • 217
  • I don't think this is a good idea because since viewDidLayoutSubviews is called on any bounds change, this will create a new constraint and activate it on every single bounds change. – Tumata Jul 25 '18 at 18:41
  • Yes, you need to make sure that if it exists you don't create another one. – Jonny Jul 26 '18 at 11:35
0

This also works:

override func viewDidLoad() {
    super.viewDidLoad()

    if #available(iOS 11.0, *) {}
    else {
        view.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height - 80).isActive = true
        view.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 20).isActive = true
    }
}
Menan Vadivel
  • 106
  • 1
  • 5
0

I added a NSLayoutConstraint subclass to fix this problem (IBAdjustableConstraint), with a @IBInspectable variable, looks like this.

class IBAdjustableConstraint: NSLayoutConstraint {

    @IBInspectable var safeAreaAdjustedConstant: CGFloat = 0 {
        didSet {
            if OS.TenOrBelow {
                constant += safeAreaAdjustedConstantLegacy
            }
        }
    }
}

And OS.TenOrBelow

struct OS {
    static let TenOrBelow = UIDevice.current.systemVersion.compare("10.9", options: NSString.CompareOptions.numeric) == ComparisonResult.orderedAscending
}

Just set that as the subclass of your constraint in IB and you will be able to make < iOS11 specific changes. Hope this helps someone.

Alex Brown
  • 1,553
  • 1
  • 11
  • 22
0

I used this one, add the top safe area layout and connect with outlet

@IBOutlet weak var topConstraint : NSLayoutConstraint!

override func viewDidLoad() {
    super.viewDidLoad()
    if !DeviceType.IS_IPHONE_X {
        if #available(iOS 11, *)  {
        }
        else{
            topConstraint.constant = 20
        }
    }
}
Suraj Rao
  • 28,186
  • 10
  • 88
  • 94
little
  • 1
  • 1
-5

Found the simpliest solution - just disable safe area and use topLayoutGuide and bottomLayoutGuide + add fixes for iPhone X. Maybe it is not beautiful solution but requires as less efforts as possible

Vyachaslav Gerchicov
  • 1,941
  • 3
  • 19
  • 43