Adding Multitasking Support to your iOS App

Fady Yecob
Just Eat Takeaway-tech
7 min readSep 28, 2021

Websites have been adapting to responsive web design for several years now. For iOS apps, fully adopting responsive design by supporting split view multitasking and slideover is not something iOS developers think about a lot. One of the reasons being that this was only introduced a few years ago in iOS 9 (September 2015).

Supporting this feature doesn’t just mean having support for iPhone and iPad, but also anything in between. So the app has to be able to adjust when the user changes the size of the app’s window. Supporting multitasking doesn’t just give your user a better experience on the iPad but could also improve the user experience on the Mac. Macs that have Apple silicon are able to run iOS apps without any developer modifications, assuming the developer does not opt-out of this feature.

This article will highlight the code changes that needed to be done in the Takeaway iOS app in order to make the app support this feature. And what needs to be done to make sure both your iPhone and iPad app still works in your single iOS app codebase.

Size classes

In order to make sure your app looks good in both small and large windows (UIWindow), you should use size classes. There are a couple of different ways to access these size classes. This is usually done by getting the traitCollection variable of a class that conforms to the UITraitEnvironment protocol. Examples of classes that conform to this protocol are:

If you’re working on code inside of a UIViewController you’re able to access all of the previously mentioned trait collections. Here are some examples of how to access them:

  • Access it from the UIViewController itself
  • Access it from the UIViewController’s view or any subview
  • Get the UIWindow and access it. E.g. view.window
  • Get your application’s window and access it. This can be done by accessing either your AppDelegate’s or SceneDelegate’s window property

Once you get the trait collection variable you have 2 different size classes, the horizontalSizeClass (a.k.a. the width size class) and the verticalSizeClass (a.k.a. the height size class). They can have 3 different values:

The vertical size class is usually only useful if your app supports landscape orientation on iPhone devices. Since the Takeaway app does not support this, we will only focus on the horizontal size class in this article.

When is it compact and when is it regular?

Since the Takeaway app doesn’t support landscape orientation on the iPhone, the horizontal size class will always be compact when running it on any iPhone device that is available at the time of writing this article. When running the app in full-screen mode on iPad the screen’s horizontal size class will be regular. But when using split-screen multitasking the horizontal size class can be either compact or regular, depending on how big the user decides to make the window. Some generic info about the different size classes can be found in Apple’s documentation. Apple’s documentation will explain which size class the system will apply.

Multiple trait collections?

So with a bunch of ways to access the trait collection variable, your question might be, which one should I use? Usually, the horizontal size class of your view controller will be the same as the one of your main UIWindow. But there are instances where this is not the case. This is the case when using childViewControllers. For example when using a UISplitViewController in the image below.

The Takeway app on the left in split view mode and Safari on the Right

In the example above the horizontal size class for both the window and the split view controller is regular. But for each of the view controllers inside the split view controller, the horizontal size class is compact. In the example above you might want to use the size class of the split-screen controller to determine whether or not to load the message on the view controller on the right. Since if the horizontal size class is compact either only the view controller on the left or the view controller on the right will be shown.

What to keep in mind

This section will mainly contain some dos and don’ts and things to keep in mind. One of the main things to avoid is using the bounds or frame of UIScreen.main for any calculations or layout. The reason for this is because when you run the app in a smaller window, the screen’s bounds will be larger than the window’s bounds. This could cause views to exceed the window’s bounds.

Avoid: checking the device instead of the trait collection

In general, you should avoid using UIDevice.current.userInterfaceIdiom to check whether the device is an iPhone or iPad. In general, it’s better to use the horizontal size class as previously explained to do this. There are cases where you do want to use userInterfaceIdiom e.g. when a feature is only available on either iPad or iPhone. This could, for example be the case when the user can only use a feature at a certain location. In the Takeaway app, this is, for example the case for the dine-in feature. Since the user needs to be able to be at the restaurant to use this feature. And users are not carrying their iPad in their pockets. A code example can be seen below.

// Avoid this, unless the feature is really iPhone only. E.g. the user has to really be in a store with their phone
if UIDevice.current.userInterfaceIdiom == .pad {
// Some custom iPad (regular size class) code
}
// Instead do this
if traitCollection.horizontalSizeClass == .regular {
// Some custom iPad (regular size class) code
}

Using collection views

A lot of developers use UICollectionViews instead of UITableViews when displaying lists. Since collection views are a lot more versatile. When using a collection view, the layout of that collection view will not be updated when changing the window size. In order for this to happen, you’ll have to call invalidateLayout() on your collection view layout, when the view controller size changes. It’s usually sufficient to override the viewWillLayoutSubviews of the view controller and call the invalidate layout function. This can be seen in the code block below.

override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
collectionView.collectionViewLayout.invalidateLayout()
}

Programmatic constraints

When creating constraints programmatically, and when you have different constraints for either compact or regular size classes, you’ll usually have to manually enable/disable these. This could also be done by overriding the previously mentioned viewWillLayoutSubviews function and enabling the constraints based on the size class. When possible it’s best to define the constraints for different size classes inside the xib file. This will make sure they’re automatically updated when resizing the window without having to create a function to disable or enable constraints. An example of enabling custom constraints for compact and regular size classes can be seen below. The example below has a carousel view that uses the full view width in the compact size class. It also has a fixed width in the regular size class to make sure it doesn’t look stretched out.

override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
activateConstraintsBasedOnSizeClass()
}
private func activateConstraintsBasedOnSizeClass() {
let isCompact = traitCollection.horizontalSizeClass == .compact
// The compact size class has a full width carousel view
// Which we activate to make sure uses the width
carouselRichtAnchor?.isActive = isCompact
carouselLeftAnchor?.isActive = isCompact
// The regular size class has a fixed width carousel view
// Which we activate to make sure the carousel doesn't look stretched out
carouselWidthAnchor?.isActive = !isCompact
}

So why override the viewWillLayoutSubviews and not the traitCollectionDidChange function? The reason behind this is because the window size could change, while the size class stays the same. When that’s the case you probably still want your view to update its layout.

The future: SwiftUI

So you might think, this UIKit stuff is all nice, but SwiftUI is the future right? So the size class values in SwiftUI are similar, but there’s a separate enum for it called UserInterfaceSizeClass which only has 2 values:

To access the horizontal size class you can use the following property wrapper below. This returns the size class of the SwiftUI view.

@Environment(\.horizontalSizeClass) private var horizontalSizeClass

Also, note that UIKit and SwiftUI can be used together. So if you still need the horizontal size class of the window or view controller, you could still use that by injecting/overriding a custom EnvironmentValue.

Is anybody even going to use it?

In order to know if users actually use this multitasking feature and whether it’s worth maintaining it, we decided to track it using Firebase. In order to track this app-wide, we created a subclass of UIWindow called AppWindow. This is the main window that is used in the Takeaway app. This custom AppWindow overrides the traitCollectionDidChange function and calls the tracking event of our custom tracking manager, with the new size class. Behind the scenes, this sets a Firebase user property.

final class AppWindow: UIWindow {
// Some more code
override func traitCollectionDidChange(
_ previousTraitCollection: UITraitCollection?
) {
super.traitCollectionDidChange(previousTraitCollection)
splitScreenTrackingManager.traitCollectionDidChange(
to: screenState
)
}
}
// In your AppDelegate/SceneDelegate
window = AppWindow(...)

Based on this tracking data there seem to be quite a good number of users using multitasking on the iPad since it was released a couple of months ago.

Just Eat Takeaway.com is hiring. Apply today!

--

--

Fady Yecob
Just Eat Takeaway-tech

iOS Software Development Engineer @ Just Eat Takeaway