Abstract away from device model when adapting UI to iPhone X

Make custom appearance for a button with Auto Layout only

Yevhen Dubinin
3 min readNov 12, 2017

Our designer recently asked to make a button at the bottom of the screen look like this:

iPhone 6,7,8 vs. iPhone X

In this post I’ll show how to approach this adaptive UI without relying on device model lookup.

The Mindset

Each year, when new devices come out, people keep asking questions on how to detect that models programmatically (2017 is also no exception).

While it is possible to do so, the API is kinda weird. That’s because Apple strives to teach us not to do it this way. Instead, they ask developers to rely on availability tests, when it comes to functionality and on Auto Layout for making custom interfaces. In other words, abstract away from particular device model. Well, in an ideal world that would probably work… still, looking at things from this angle actually does help sometimes.

The Safe Area

Upgrade, while going back to Home screen :)

First thing I did, was changing the button’s constraints to adhere to safe area of the main view controler’s view.

This instantly fixed the issue we had had during initial launch of the app in iPhone X Simulator and Xcode 9.

NSLayoutConstraint *buttonBottomConstraint = nil;
if (@available(iOS 11, *)) {
buttonBottomConstraint = [self.button.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor];
}
else {
buttonBottomConstraint = [self.button.bottomAnchor constraintEqualToAnchor:self.bottomLayoutGuide.topAnchor];
}

NOTE: If you wonder about bottomLayoutGuide.topAnchor, this is because bottomLayoutGuide has id<UILayoutSupport> type. Not helpful? Well, that’s out of scope of this story. Sorry for that :(

Then, I decided to redone all the constraints of the button, to adhere to safe area:

NSLayoutConstraint *buttonBottomConstraint = nil;
NSLayoutConstraint *buttonLeadingConstraint = nil;
NSLayoutConstraint *buttonTrailingConstraint = nil;
if (@available(iOS 11, *)) {
buttonBottomConstraint = [self.button.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor];
buttonLeadingConstraint = [self.button.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor];
buttonTrailingConstraint = [self.button.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor];
}
else {
buttonBottomConstraint = [self.button.bottomAnchor constraintEqualToAnchor:self.bottomLayoutGuide.topAnchor];
buttonLeadingConstraint = [self.button.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor];
buttonTrailingConstraint = [self.button.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor];
}
buttonHeightConstraint = [NSLayoutConstraint constraintWithItem:self.button attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.f constant:50.f];// we will need references to these constraints later
self.buttonConstraints = @[buttonBottomConstraint, buttonLeadingConstraint,buttonTrailingConstraint,buttonHeightConstraint];

These constraints are created only once, during loadView routine and added to self.view.

Looks better, but still not perfect

Margin and Rounded Corners

If you think about this for a minute, what really makes button look differently is its distance from the bottom of the screen, not the device model: when there is no space between bottom edge of the button and the screen — it is OK to have it with no margin and rounded corners.

To find out the distance I utilize self.view.safeAreaInsets.bottom value

- (void)adjustButtonAppearanceIfNeeded {    // if the button is not at the very [visual] 
// bottom of it's superview (hint: iPhone X)
// then make the rounded corners
if (@available(iOS 11, *)) {
CGFloat bottomPadding = self.view.safeAreaInsets.bottom;
if (bottomPadding > 0) {
if (self.button.layer.cornerRadius != 10.f) {
self.button.layer.cornerRadius = 10.f;
self.button.layer.masksToBounds = YES;
self.buttonConstraints[1].constant = 8; // leading
self.buttonConstraints[2].constant = -8; // trailing
[self.button setNeedsDisplay];
[self.view setNeedsLayout];
}
}
else {
if (self.button.layer.cornerRadius != 0.f) {
self.button.layer.cornerRadius = 0.f;
self.button.layer.masksToBounds = NO;
self.buttonConstraints[1].constant = 0; // leading
self.buttonConstraints[2].constant = 0; // trailing
[self.button setNeedsDisplay];
[self.view setNeedsLayout];
}
}
}
}

To make sure, bottomPadding value is correct, this method should be called from viewDidLayoutSubviews:

- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self adjustButtonAppearanceIfNeeded];
}

Notice, the condition comparing cornerRadius value with either 10.f or 0.f. This is in order to prevent endless layout cycle after [self.view setNeedsLayout].

Final result more or less expected.

Conclusion

Safe area concept is much cleaner than layout guides.

--

--