This workshop will be retired on May 31, 2020.
Complex Width Views: Code14:00 with Pasan Premaratne
In this recipe we're going to build a two view layout where each view occupies a varying width depending on the available space.
In this recipe, we're going to build a two view layout, 0:00 where each view occupies a varying width depending on the available space. 0:03 We'll have two views, a red and a blue view, 0:08 where we want the red view to be double the width of the blue view if possible. 0:12 The catch is that the blue view can't be too narrow either. 0:17 And we want to enforce a minimum width. 0:21 We'll need to use a combination of inequality relations and 0:24 constrained priorities to build this relationship. 0:28 I've gone ahead and added a file to my project named ComplexWidthViews. 0:31 And in here I have a custom UIViewController subclass. 0:36 So inside this class, we have some starter code that defines a blue and 0:41 a redView, and that's really all I've done here. 0:46 In viewDidLoad, I've set these views to have the right background colors. 0:49 And then the first thing we always want to do when setting constraints in code 0:53 is to disable autoresizing masks. 0:56 This is handled automatically in interface builder, 1:00 but it's important to be aware of this in code. 1:02 Otherwise you'll end up with constraints you didn't add, 1:05 which will break your layout. 1:08 So I've added that inside viewWillLayoutSubviews, and 1:10 then I've added the views as subviews of the root view. 1:12 So ensure you do this step every single time 1:17 before you activate any constraints in code. 1:20 Now for this layout, we won't be doing anything special in the vertical axis, so 1:24 let's get some basic constraints in place. 1:28 We'll send vertical spacing constraints of eight points to the top and bottom for 1:31 both the views. 1:35 I'm building this layout in XCode 9 against the iOS 11 SDK, 1:37 which means I can make use of the new safe area layout guide APIs. 1:41 But we also need to ensure that this builds for versions prior to iOS 11. 1:46 So we're going to use the if available syntax to target all the different cases. 1:50 We'll start by saying if #available, so if #available, 1:55 and the platform is iOS and the version is 11. 2:00 And if we're in version 11 of iOS, 2:04 then we'll use as a guide the view's safeAreaLayoutGuide. 2:06 Otherwise we'll execute some code in the else case. 2:13 In the iOS 11 case, we can use the view.safeAreaLayoutGuides, 2:18 like I mentioned. 2:22 But in any other case, prior to iOS 11, we'll fall back to the standard top and 2:23 bottom layout guides or margins. 2:28 The first set of constraints affected by this are the top and 2:31 bottom constraints on the blue view, so let's add those in. 2:33 We'll use the NSLayoutConstraint class, and 2:37 we'll call a class method activate that takes a series of constraints. 2:40 So we'll say blueView.topAnchor.constraint(equalTo: 2:45 guide.topAnchor, constant: 8.0). 2:52 We also want to add a similar constraint to the bottom anchor to space it out, 2:59 so constraint(equalTo: guide.bottomAnchor, constant: -8.0). 3:05 This time, making sure it's negative to take into account the direction. 3:11 Okay, so this is the case for the safeAreaLayoutGuides. 3:16 In the else case, we'll create another NSLayoutConstraint, 3:19 where we'll call the activate method here. 3:23 And this time, so we'll copy this constraint over, so blueView.topAnchor. 3:26 But instead of going to the guide's top anchor, 3:32 here we'll say topLayoutGuide.bottomAnchor. 3:35 And then we'll copy paste the first one, or rather the second one, 3:39 the bottom anchor constraint. 3:43 And here we'll say bottomLayoutGuide.topAnchor. 3:45 Okay, remember we're doing an 8 point vertical space constraint 3:52 on the redView as well, so we need to mimic these. 3:56 And we can simply just copy these over, I'll leave data, in here. 3:59 And then we'll paste, and here, we'll say redView for each of these. 4:04 And that should be it. 4:11 And so we can do the same right here. 4:12 Copy, paste, and we will change this to redView. 4:16 That takes care of the vertical constraints. 4:22 In the horizontal axis, regardless of the width of the views, 4:25 we're going to space them 8 points on either side. 4:29 So let's add those constraints in before we define any of the width constraints. 4:32 In iOS 11 and above, 4:38 we'll add these constraints from the subviews to the safe area. 4:39 While in versions prior to iOS 11, we'll add them to the super view. 4:43 So I will put that right here and we'll say blueView.leadingAnchor. 4:47 You can do left anchor because there's no content in here really, but 4:52 we'll just go ahead with the leadingAnchor. 4:55 And we'll say constraint(equalTo: 4:57 guide.leadingAnchor, constant: 8.0). 5:01 Then we'll do redView.trailingAnchor.constraint(equalTo: 5:06 guide.trailingAnchor, constant: -8.0). 5:14 You'll notice that I'm not adding the blueView's trailing constraint here. 5:20 And that's because safe area doesn't matter there. 5:23 Now we're going to add that to the red view, so 5:26 we can do it outside this if else clause. 5:28 Okay, we need to do the same thing at the bottom, so add a comma. 5:31 And we'll say blueView.leadingAnchor.constraint(equalTo: 5:34 view.trailingAnchor, right? 5:41 There's no layout guide here, so we're adding directly to the root view. 5:44 With a constant value of 8 points, and then finally 5:49 redView.trailingAnchor.constraint(equalTo: view, 5:54 oops, view.leading for the blue. 6:00 Make sure you correct that as well, and then view.trailing for the red. 6:04 The constant value here is -8.0. 6:08 We have one horizontal space constraint to add between the two views. 6:12 But again, since this does not depend on either the safe area layout guides or 6:16 any traditional layout guides, 6:20 we can add it to the hierarchy outside the if else block. 6:22 So NSLayoutConstraint.activate, and 6:25 this constraint is blueView.trailingAnchor.constraint(equa- 6:29 lTo: redView.leadingAnchor, constant: -8.0). 6:35 With these spacing constraints out of the way, let's focus on our width constraints. 6:40 We'll start by setting a with constraint on the blueView. 6:45 Remember when we said that while we want the blueView to adjust its size to 6:49 accommodate a growing redView, we don't want it to get too narrow. 6:53 So we're going to enforce a minimum width constraint by setting a greater than or 6:56 equal relation. 7:01 So we'll say blueView.widthAnchor.constraint, now make 7:02 sure you pick greater than or equal to constant here, and we'll say 150 points. 7:07 Now this constraint alone is insufficient information. 7:15 Now it's hard to kind of imagine this, but the blueView, 7:19 since it's allowed to grow more than 150 points, it could take up nearly all of 7:22 the available space, shrinking the red view down to a 1 point width. 7:27 And technically our layout would be satisfied. 7:31 It could also fit a wide range of widths between where we're at now, so let's say 7:34 the blue view is 150, and up to where the width allows for a 1 point wide red view. 7:40 So that's not what we want, so we need to add more constraints. 7:46 Unfortunately this is hard to visualize, like I've said. 7:49 If you've taken the interface builder version of this video, 7:51 you can see that visually. 7:54 But here we just kind of have to imagine. 7:56 To fix this, we need to define some constraints 7:58 that allows auto layout to determine a width for the red view. 8:01 With a width for the redView, we can then figure out what the width for 8:05 the blueView needs to be. 8:08 In this particular recipe, 8:10 we want the redView to be twice as wide as the blueView if possible. 8:12 I say if possible because we can only do that up to a certain limit. 8:16 In the portrait orientation, with the minimum width constraint, 8:21 there's not enough room to accommodate a redView that's twice as wide. 8:24 But that's okay, in that case, 8:28 we would want the redView to just fill up the remaining space. 8:30 But in the case that there is available space to grow up to twice as wide, 8:33 then we want it to do so. 8:38 This seems complicated, but it can be achieved with one constraint. 8:40 And instead of defining this constraint inside the array directly like we've been 8:45 doing, I'm going to assign it to a local constant, so we'll do it up here. 8:49 So we'll say, let redViewWidthConstraint = 8:52 redView.widthAnchor.constraint(equalTo:. 8:57 Now we're not going to select the constant one, we're going to look for 9:03 the method that takes a multiplier. 9:06 So we'll say equalTo: blueView.widthAnchor. 9:08 And the multiplier is 2.0, so we want it to be twice as wide as the blueView. 9:13 Let's add this constraint to our array of activated constraints, so 9:19 comma redViewWidthConstraint. 9:23 If you run the project now, the layout will still be broken. 9:25 And that's because with our new constraint, we're violating the minimum 9:30 width constraint on the blue view, at least in portrait mode. 9:33 In portrait mode, we cannot have a situation where the blue view has 9:37 a minimum width of 150 points and where the red view is twice that or 300 points. 9:41 There's just not enough space. 9:47 So the layout is unresolved. 9:49 To fix this, the last adjustment we need to make is to drop the priority 9:52 on this redViewWidthConstraint to 750, or 9:56 to make it high in the default or the predefined values. 10:00 This is why we assigned the constraint to a local constant. 10:04 We can only change priorities on a constraint after it has been created. 10:07 So we'll say redViewWidthConstraint.priority. 10:11 Now you can simply say 750 here. 10:15 But that's not what we want. 10:18 Well, we do want that, but there's an easier way. 10:19 We can simply say .defaultHigh. 10:21 So this is actually a type UILayoutPriority. 10:24 Click here, so we can go see it. 10:29 So jump to definition, priority, and 10:31 you'll see that UILayoutPriority is a struct. 10:33 And you can initialize it with a float, 10:38 which is why we were able to assign 750 directly to it. 10:41 But it also has these static properties required, defaultHigh, 10:45 defaultLow, and so on. 10:47 So we're saying defaultHigh, that avoids us being magic numbery. 10:48 By lowering the priority on the proportional width constraint, 10:54 we've effectively made it act like a less than or equal to constraint. 10:57 Since the greater than or equal constraint has a higher priority, auto layout will 11:02 first make sure that a minimum width of 150 points is enforced for the blue view. 11:07 Having a lower priority than 1000, the proportional width 11:13 constraint in a scenario of conflict is treated as optional. 11:17 Which means that along with the horizontal spacing constraints on either side, 11:21 auto layout will simply set the red view's width as the remaining available space. 11:26 This means that technically the red view can be any width 11:31 less than double the blue view's width and at most the blue view's width, 11:34 which is essentially what a less than or equal relation would define. 11:39 It is important to note however that while it might seem that way, 11:43 auto layout does not view it as a less than or equal relationship. 11:48 If we were to modify this proportional width constraint that we've defined here 11:53 to have a less than or 11:57 equal relation with the priority of 1,000, the layout will not be satisfied. 11:58 So the key here is that for our layout to work, the lower priority allows auto 12:03 layout to ignore the constraint when it cannot accommodate it. 12:08 Let us see what this looks like. 12:12 Head over to Main.storyboard, select the view controller in here. 12:14 And in the backing class in the identity inspector, 12:19 we'll switch it to what we just wrote, which is ComplexWidthController. 12:22 And let's run it. 12:28 So you can see here, in the portrait mode, 12:30 we're enforcing the minimum width constraint on the blue view. 12:33 So there's not enough space here for the red view to be twice as wide, so 12:37 we've set the blue view to 150 points. 12:41 And then the red view simply fills up whatever available space is left. 12:43 Now, if we switch to landscape orientation, 12:48 we can see how these constraints adapt. 12:50 In this view we see that the red view is double the width of the blue view. 12:53 But what's interesting, and you can't really measure it here, but 12:57 what is interesting is that the blue view is wider, as well. 13:00 Since the blue view has a greater than or 13:04 equal constraint in this orientation, there's enough room for 13:06 it to grow after the minimum width constraint has been satisfied. 13:10 Effectively what this allows auto layout to do is to take the available space 13:15 after the horizontal spaces between the views have been deducted and then allocate 13:19 two-thirds of that space to the red view and then one-third to the blue view. 13:24 When it does this, it allows the red view to be double the width of the blue view. 13:28 And after doing this, since there's more than 150 points of space available 13:33 in that one-third allocation, the blue view can grow to fit this space 13:38 since its width is defined using a greater than or equal constraint. 13:42 So here we have a complex width scenario 13:47 that was achieved with two constraints in the horizontal direction. 13:49 If you're interested in learning how to do this in interface builder, 13:53 check the notes section for a link to this video. 13:57
You need to sign up for Treehouse in order to download course files.Sign up