In the very early days of developing for iOS (or “iPhone OS” as it was then called), designing a UI was relatively simple - with just one screen size to design for, it was possible to create a pixel-perfect design. Fast-forward to today and there are many more screen sizes, making old UI approaches completely unscalable. Apple has introduced a few different techniques to deal with this fragmentation however, primarily the concept of Adaptive Layout.
In this talk at GOTO Conference CPH 2015, Sam Davies dives into what Adaptive Layout is and runs through several live examples to cover the concepts of Adaptive Layout, as well as some tips for using Interface Builder. He also discusses some best practices to deal with multiple screen sizes, taking inspiration from the web, Android, and iOS.
Introduction (0:00)
My name is Sam. I am @iwantmyrealname on the Twitters, and I work for a company called Razeware that is responsible for the small company behind the massive team effort that goes on for raywenderlich.com. Let’s talk about some Adaptive UI!
In The Beginning, There Was… (1:16)
Back in the dark ages when we developed for “iPhone OS,” as it was called, back in the dark ages, there was one size we had to build for: the nice, old 3.5 inch iPhone, and designing layouts was easy. Realistically, you did also have to deal with landscape, but that’s only two sizes. If you were on an iPhone, more often than not, you would just say you’re not allowed to do it landscape, it has to be portrait. Those were the days.
Then came along the iPad, this massive slate of stuff that was revolutionary. The difference in size between the iPhone and the iPad was huge. You could just build two completely separate apps, or potentially build the same app and do different layouts, but they were two separate apps.
Then came along the 4 inch iPhone, with the iPhone 5 and 5s. This was the same width as the 3.5 inch, it just got this extra little bit at the bottom. Perfect size to stick an ad in, and that was quite often what happened. Just stick something along the bottom, It doesn’t really matter. Quite often, if you still had a 3.5 inch phone, the design was for the four inch, and you would just end up with losing some stuff off the bottom and the app just wouldn’t work. Nobody really felt the need to worry about that kind of thing, we didn’t care about these people with old phones.
Then along came the iPhone 6 last year, copied from the form factor of Android. That was at 4.7 inches, and then the 5.5 inch, the massive iPhone 6 Plus, and 6s Plus now, copied from the world of dinner plates.
Finally, coming up soon, the iPad Pro is enormous, and it is yet another form factor we’ve got to deal with.
If you account for portrait and landscape, that is 12 different form factors that you’ve got to design for now.
In days gone by, we would’ve written code like this:
if UIDevice.currentDevice().userInterfaceIdiom == .Pad {
if UIDevice.currentDevice().orientation == .LandscapeLeft ||
UIDevice.currentDevice().orientation == .LandscapeRight {
doSomething()
} else {
doSomethingElse()
}
} else {
if UIDevice.currentDevice().orientation == .LandscapeLeft ||
UIDevice.currentDevice().orientation == .LandscapeRight {
yetAnotherAlternative()
} else {
theFourthWay()
}
}
You would just investigate in your code what type of device was being used, then go and write some layout code. Or, maybe we’re in landscape, then we need to do some other particular bit of code. This behavior will not scale. You cannot continue to do that for 12 different form factors; it just doesn’t work.
Introducing Adaptive Layout (4:53)
This is why Apple released Adaptive Layout, a layer on top of the existing way that we do layouts. It abstracts the layout away from design specifics. We no longer care about the device type or orientation. Instead, we group all of these concepts together in hand-wavy things called “size classes.”
What are size classes? Rather than saying “You’ve got this many pixels or this many points,” we’re talking in terms of how much space there really is. How much room have we got to fit things in?
We divide size classes up into two different categories: compact or regular. Compact means there’s not much space, and that we’re restricted in some kind of way. Regular means there’s a normal amount of space, whatever that means.
We also talk about size classes in two different dimensions: horizontal or vertical. For example, you can be horizontally compact, and vertically regular. It’s just a way of specifying, the amount of space in a particular view.
Size Classes on Devices (7:34)
How do these concepts map to actual devices?
Horizontal | Vertical | |
---|---|---|
iPad Portrait | Regular | Regular |
iPad Landscape | Regular | Regular |
iPhone Portrait | Compact | Regular |
iPhone Landscape | Compact | Compact |
iPad 6[s] Plus Landscape | Regular | Compact |
When you take an iPad or an iPad Pro, it’s always regular in both dimensions. There’s always loads of space vertically and horizontally; there’s never any time on an iPad where you can’t fit the amount of content in that you want.
However, when you look at an iPhone in portrait, we say that it’s compact width; there isn’t much room, width-wise, on an iPhone. Then, when you turn it landscape, there’s not much space vertically. That’s now compact. If you’re reading lots of content or something on an iPhone in portrait, there’s plenty of room because you scroll up and down, but if you rotate it to landscape, you don’t scroll left and right. Nobody ever reads something and then scrolls all the way across to one side, and then scrolls all the way back to read the beginning of the next line. So, can say that it’s compact.
With iOS 9, this starts to make a little bit more sense with the advent of multi-tasking. For example, an iPad is always regular/regular, irrespective of which orientation it’s held in. However, when you start doing multi-tasking, which is new in iOS 9, you can swipe in from the right and have another app come in on the right hand side. If you’ve got a new iPad, you can pull it further across and have two apps side by side. At that point, effectively, you’ve got an iPad running two iPhone apps next to each other. It may be an iPad app, but running in the configuration it would use if it were on an iPhone, with compact horizontal size class and regular vertical size class.
Then, in iPad Pro, I believe you can have two regular/regular apps side by side. By this point, we’ve abstracted it away from just device-specific dimensions. We can now have different things running on a device.
Adopting Adaptive Layout (11:16)
What is a sensible process for adopting Adaptive Layout? The end goal is that I create one storyboard that rules everything. You can use one storyboard that will run on an iPad, an iPhone, and all of the different iOS devices. We no longer have this problem where we’ve got four different storyboards, and we have to make sure we update every single one of them. Updates to one of them will then mean that everything updates appropriately. How do you go about it? I have a five-step approach I would recommend.
- Build Base Layout
- This is the “let’s get everything that we want on the screen” step, or the layout that we want to happen most of the time.
- Choose Size Class Override
- Uninstall Irrelevant Constraints
- We’re talking Auto Layout here. You’ve got these constraints which determine the size and position of different views, and you can do this thing called uninstalling them. We’ve chosen a particular size class, and I want to take these constraints and throw them away.
- Add New Constraints Specific to Size Class
- This is to make sure that we get the layout we actually want in this new size class.
- Rinse and Repeat
- The important thing is to not go into your storyboard and build an iPhone portrait layout, then an iPhone landscape layout, then throw everything away before you move to iPad, etc. That doesn’t really help you at all. The approach is to start with a base layout, then work on top of it.
Demo (13:22)
I gave a demo of this approach at GOTO Copenhagen 2015, which you can watch above. I explain how to shift a layout across different devices from a single base layout by installing constraints.
What’s Adaptive? (25:34)
What types of things are adaptive? Constraints, for one. You can take a constraint and you can decide if you want it in this particular size class or not. That way, you can realign and reorganize your layout in many different ways, which is cool. But that’s not all that you can do with Adaptive Layout! Other things are adaptive as well.
You can also change the constant on a constraint. If you have a constraint that says, “The spacing between two views should be ten points,” then I can say, “If I’ve got enough space, then actually that should be a hundred points.” I can do that without having to delete that constraint and create a new one. Weirdly, you can’t change the constraint multiplier, though: if you need to change the multiplier, then you do actually have to uninstall the constraint and create a new one.
You can also change the font. If between an iPhone and iPad I want to change the font size to make it bigger on an iPad, I can do that fairly simply.
Finally, view installation, which is also quite important. If you have a layout for an iPhone that you want to reuse on an iPad, you’re probably not just going to want to change the font size and the spacing. You’re quite likely to want to add new views too, which is easy as well.
Size Classes and Fonts Demo (27:23)
Here is another demo I gave about changing size classes and fonts. Click here to watch it above!
Doing Battle With Code (31:12)
How does this all work in code? You can get a long way in Interface Builder, but you’re going to want to get in there and do battle with code eventually.
public class UITraitCollection : NSObject, NSCopying, NSSecureCoding, NSCoding {
...
public var userInterfaceIdiom: UIUserInterfaceIdiom { get }
public var displayScale: CGFloat { get }
public var horizontalSizeClass: UIUserInterfaceSizeClass { get }
public var verticalSizeClass: UIUserInterfaceSizeClass { get }
@available(iOS 9.0, *)
public var forceTouchCapability: UIForceTouchCapability { get }
}
All this stuff exists in this new class alled UITraitCollection
, which was introduced last year. It is now the place to find out different things about the device, including the user interface idiom (is it an iPhone, and iPad, etc?). You can get the display scale, which will give you one, two or three, depending on the number of pixels per point. You can get the two size classes, so if I can get hold of a traitCollection
, I can find out what my current size class is. Then finally, if you’ve got an iPhone 6s or a 6s Plus, you can find out whether or not you’ve got 3D Touch, so you can determine how hard you’re pushing your finger through the screen.
You get a traitCollection
by using trait environments. UITraitEnvironment
is just a protocol that has a traitCollection
property on it.
public protocol UITraitEnvironment : NSObjectProtocol {
public var traitCollection: UITraitCollection { get }
public func traitCollectionDidChange(previousTraitCollection: UITraitCollection?)
}
UIScreen
, UIWindow
, UIPresentationController
, UIViewController
and UIView
adopt the protocol, so that means if you’re inside any of those things, you can find out what your current traitCollection
is, and hence, what size class you’re in. You just have to ask for the traitCollection
, and then you’ll have what you need to know.
You’ll also notice this traitCollectionDidChange
function. That would get called whenever the traitCollection
has changed, but when would that happen? If I took an iPhone in portrait and rotated it, then the traitCollection
of every view, view controller, presentation controller, the screen and the window will all receive traitCollectionDidChange
because you’ve rotated it. You’ve gone from regular height, compact width, to compact height, compact width, or, on an iPhone Plus, regular width. You could use that to handle rotation or to handle any code-type things that you want to do when the traitCollection
is altered.
Overriding Size Classes (33:44)
You can override size classes, but why would you want to do this?
extension UIViewController {
public func setOverrideTraitCollection(collection: UITraitCollection?,
forChildViewController childViewController: UIViewController)
public func overrideTraitCollectionForChildViewController(
childViewController: UIViewController) -> UITraitCollection?
}
You could have built a view controller that has a particular layout for a given size class, and then you realize, “I’m on an iPad, but I’ve built a container view controller and I’m putting this other view controller in it. I defined the layout for compact width, because this view controller is so small, so I want a container view controller.”
In this case, you can use this traitCollection
instead for this particular child view controller. I’m on this massive canvas of an iPad, but one of my child view controllers is quite tiny (i.e. compact width).
You could use the code above, and it’s quite simple to use: you build yourself a traitCollection
with the overrides in it that you want, and you pass it to the child view controller through this method, which is on UIViewController
.
UIContentContainer
(34:46)
The last protocol I want to introduce is UIContentContainer
. This is a slightly more fine-grained way of dealing with transitions between traitCollection
.
public protocol UIContentContainer : NSObjectProtocol {
...
public func viewWillTransitionToSize(size: CGSize,
withTransitionCoordinator coordinator:
UIViewControllerTransitionCoordinator)
public func willTransitionToTraitCollection(
newCollection: UITraitCollection,
withTransitionCoordinator coordinator:
UIViewControllerTransitionCoordinator)
}
Say you get a traitCollectionDidChange
. All of a sudden, you’re just being told the traitCollection
has changed, re-lay yourself out. How do you deal with making sure you’re handling the animations in a nice way? That’s where you want to use these methods that are on UIContentContainer
.
UIContentContainer
is adopted by UIViewController
and UIPresentationController
. These have this willTransitionToTraitCollection
. Before the transition happens, you get told you’re going to move to this traitCollection
, and within that, you get a transitionCoordinator
.
transitionCoordinator
allows you to say, “I want to do an animation, and I want to do it at the same time as whatever animations the system is doing.” This is really handy.
The other method on here is viewWillTransitionToSize
. The question that everybody asks is, “My iPad is regular/regular irrespective of the orientation? That doesn’t make any sense.” This viewWillTransitionToSize
method is helpful here. This gets called whenever the view controller changes size. Before iOS 9, that would only be on rotation, unless you were doing some complex view controller containment yourself. When you rotate an iPad, the top method of that will be called. Because the size will change, the bottom method won’t be called.
Rotation Deprecation (36:41)
extension UIViewController {
@available(iOS, introduced=2.0, deprecated=8.0)
public var interfaceOrientation: UIInterfaceOrientation { get }
@available(iOS, introduced=2.0, deprecated=8.0,
message="Implement viewWillTransitionToSize:withTransitionCoordinator: instead")
public func willRotateToInterfaceOrientation(
toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval)
@available(iOS, introduced=2.0, deprecated=8.0)
public func didRotateFromInterfaceOrientation(
fromInterfaceOrientation: UIInterfaceOrientation)
@available(iOS, introduced=3.0, deprecated=8.0,
message="Implement viewWillTransitionToSize:withTransitionCoordinator: instead")
public func willAnimateRotationToInterfaceOrientation(
toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval)
}
In iOS 8, these lovely old methods for dealing with rotation were all deprecated. You shouldn’t be using willAnimateRotationToInterfaceOrientation
or didRotateFromInterfaceOrientation
. But how do you deal with rotation now?
Use willTransitionToSize
instead. Here is an example of that method being used:
override func viewWillTransitionToSize(size: CGSize,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
let image = imageForAspectRatio(size.width / size.height)
coordinator.animateAlongsideTransition({
context in
// Create a transition and match the context's duration
let transition = CATransition()
transition.duration = context.transitionDuration()
// Make it fade
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.type = kCATransitionFade
self.backgroundImageView.layer.addAnimation(transition, forKey: "Fade")
// Set the new image
self.backgroundImageView.image = image
}, completion: nil)
}
Don’t think of rotation as moving a device. Instead, think of it as your view controller changing size, because from the user’s perspective, that is what is happening. This example here uses a transition coordinator, and it calls animateAlongsideTransition
on there. That allows it to say that when the rotation happens, the system will take your view controller and rotate it and re-size it for you in an animation.
Stack Views (37:57)
Stack views are new in iOS 9. If you haven’t used Auto Layout before, now’s a good time to get into it, because stack views will save you a lot of grief. Imagine I have a white view with three views inside it. How would I do that with constraints?
First, I’d need to pin the top one to the top and to the left and right hand sides. I’d need to pin the bottom one to the bottom. Then I’d need to add some constraints to space them as well. I’d also align them all along the middle, so that they’re all center aligned with each other. Finally, I want to specify their relative widths, and maybe say that the middle one will just use its intrinsic content size. That’s a lot of constraints, especially to build something so simple.
With stack views, I can reduce this effort from about 12 constraints to just a few. A stack view has properties on it, so I tell it what axis I’d like it to be oriented on, and I set some things like the spacing. I can say that they’re all aligned down the middle. I do have to use some constraints, because I have to position this stack view somewhere within its wider view. I could even add two more constraints if I wanted to pin it exactly to a specific size.
In Xcode, learn to love this button that looks like an arrow falling down stairs. That creates a stack view. From there, you can alter all kinds of different things.
The interesting thing about stack views is that they play very well with adaptivity. That means I can add size class overrides for things like the axis. For example, I can change it from being vertically aligned to horizontally aligned just by adding a size class override on the stack view. I can also change the alignment, distribution, and spacing really, really simply using adaptivity. They are quite really quite powerful when used with adaptivity, and are definitely worth a look.
Adaptivity Tips (41:17)
- Get to Know Auto Layout
- There is a bit of a learning curve, but it’s not impossible, and it’s worth the effort. It’s not as hard as it might seem at first.
- Use Adaptive Layout for Broad Strokes
- You can’t expect to be able to do all of your layout using these adaptivity tools. They are there for you to get the layout pretty much right, then you can drop into code and start using the fine grain things that you want to do, like that view or transition to size stuff.
- Start with Base Layout and Then Override
- Never ever, ever go into a storyboard and say, “Well, I want an iPad, so I’m going to start with regular/regular. Now, I want to do an iPhone in portrait so I’m going to go regular/compact.” Instead, start the base layout with nothing, and then work out, thinking about what you want to change for this particular override.
- Life is Easier with Stack Views
- If you can use iOS 9, go and investigate them. If you can’t, there are some open source things out there that will be equivalent. They make layout so much easier. If you nest stack views together, it will make life so much easier.
Now is the time to get Adaptive. As I said, you’ve got 12 different layouts to do at the moment. You could do that with several different apps, you could do that with multiple storyboards. Give Adaptive Layout a go, and see whether or not you can get anywhere.
As a reminder, I am @iwantmyrealname on the Twitter, and you can grab the code for the demos I mentioned above at my GitHub.
Q&A (43:01)
Q: How do you deal with assignments? We’ve been used to them wanting everything pixel perfect.
Sam: That’s one of the major challenges associated with this adaptivity, and it’s something I think the web world went through several years ago with this kind of idea. I remember when I first did web design, I spent a very long time trying to make it look pixel perfect between Firefox and Internet Explorer and…I guess it was before Chrome, so Opera or somethin. You’d be there trying to work out why x was not identical to y, and eventually we seemed to have gotten over that phase into this idea that the content is the important part.
But what we don’t necessarily have is, I want a pixel perfect design here, here and here. That certainly seems to have worked in the web world. I think we need to do the same kind of thing. It’s all to do with education. If you say to your designer, “Yes, you can have pixel perfect designs, now design me 12 different designs, or more in fact.” If you tell them that they’ve got to design 20 different pixel perfect designs for that one app, then they might start to get some kind of idea of what this adaptivity does.
It becomes a matter of what elements you want in the design, i.e. “When it gets this narrow, how should we rearrange it?” Because that’s exactly what happens in the web, right? You lose that big menu bar across the top, and it becomes a hamburger drop-down thing that takes up the entire screen on an iPhone. That’s not necessarily the right solution, but the only way of doing is it is to demonstrate this stuff. Demonstrate getting out of the pixel perfect world and into a focus on content, while trying to make it look as good as we can in these different ways.
Receive news and updates from Realm straight to your inbox