In order to implement the layout for regular-sized devices, the traitCollection property of the details view controller is used. As mentioned earlier, the traitCollection property contains information about the current environment your app is running in. All UIView instances, UIViewControllers, UIWindows, UIPresentationControllers, and UIScreens conform to the UITraitEnvironment protocol.
This protocol dictates that all objects that conform to UITraitEnvironment must have a traitCollection attribute. They must also have a traitCollectionDidChange(_:) method. This method is called whenever the traitCollection attribute changes. This could happen if a user rotates their device or when a multitasking window on the iPad changes its size. You'll use traitCollection and traitCollectionDidChange(_:) to correctly adapt the layout.
First, you must update viewDidLoad so it applies the correct layout for the current traitCollection attribute as soon as possible. Then, the code should watch for changes in the traitCollection attribute and update the constraints accordingly. Start by adding the following two variables to ContactDetailController.swift:
var regularWidthConstraint: NSLayoutConstraint! var regularHeightConstraint: NSLayoutConstraint!
These variables will hold the larger width and height constraints for contactImage. Now, update viewDidLoad as follows; only areas where you should update the code are included in this snippet:
// unchanged implementation compactWidthConstraint = contactImage.widthAnchor.constraint(equalToConstant: 60) compactHeightConstraint = contactImage.heightAnchor.constraint(equalToConstant: 60) // 1 regularWidthConstraint = contactImage.widthAnchor.constraint(equalToConstant: 120) regularHeightConstraint = contactImage.heightAnchor.constraint(equalToConstant: 120) // unchanged implementation allConstraints.append(centerXConstraint) // 2 if traitCollection.horizontalSizeClass == .compact && traitCollection.verticalSizeClass == .regular { allConstraints.append(regularWidthConstraint) allConstraints.append(regularHeightConstraint) } else { allConstraints.append(compactWidthConstraint) allConstraints.append(compactHeightConstraint) } NSLayoutConstraint.activate(allConstraints)
The first modification is to create two new anchor-based constraints. These constraints represent the image's size for larger screens. The second step is to check the current traits and make sure that both the horizontal and the vertical size classes are regular. Size classes in code work the same as they do in Interface Builder. When you implemented this layout in Interface Builder, you only wanted the image view to be bigger on devices that were of regular width and regular height, so this still applies. By selectively appending these constraints to all the constraints array, you can immediately apply the correct layout.
When a user is using an iPad, an app can suddenly change from a regular x regular environment to a compact x regular environment when multitasking is used. To adapt the layout accordingly, you need to implement the traitCollectionDidChange(_:) method. By implementing this method, you can check the new and old traits and decide whether to activate or deactivate certain constraints. Add the following method to ContactDetailViewController to implement this behavior:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) // 1 guard let previousTraitCollection = previousTraitCollection, (previousTraitCollection.horizontalSizeClass != traitCollection.horizontalSizeClass || previousTraitCollection.verticalSizeClass != traitCollection.verticalSizeClass) else { return} // 2 if traitCollection.horizontalSizeClass == .regular && traitCollection.verticalSizeClass == .regular { NSLayoutConstraint.deactivate([compactHeightConstraint, compactWidthConstraint]) NSLayoutConstraint.activate([regularHeightConstraint, regularWidthConstraint]) } else { NSLayoutConstraint.deactivate([regularHeightConstraint, regularWidthConstraint]) NSLayoutConstraint.activate([compactHeightConstraint, compactWidthConstraint]) } }
The first step in this code is to check whether an old trait collection exists since the method receives an optional UITraitCollection object. This means that the argument must first be unwrapped using either a guard let statement of an if let statement. In this case, guard let is used. In the same guard, the new size classes and the old ones are compared to make sure something related to the size classes has changed.
The second step is to check the new traits to see whether the app is running in a regular x regular environment. When this is the case, the compact constraints are deactivated and the regular constraints are activated. If the app is not running in a regular x regular environment, the regular constraints are deactivated and the compact constraints are activated.
Implementing an adaptive layout in code requires more work than in Interface Builder. However, the tools make it fairly straightforward. Most of the time, you'll find that Interface Builder works perfectly fine, but when you find that you need more control, you can drop down to the code level and take it from there. The layout you have implemented right now uses quite some constraints. Luckily, you can refactor the detail page to have fewer constraints by using a UIStackView object. The next section will show you how to do this.