Abstract away from device model when adapting UI to iPhone X
Make custom appearance for a button with Auto Layout only
Our designer recently asked to make a button at the bottom of the screen look like this:
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
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 becausebottomLayoutGuide
hasid<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
.
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]
.
Conclusion
Safe area concept is much cleaner than layout guides.