This workshop will be retired on May 31, 2020.
Build a Form: Code18:43 with Pasan Premaratne
In this recipe we're going to build a form that contains a series of labels and text fields. Labels are right aligned and fit the length of the label with the longest text.
In this recipe, we're going to build the form like the one you see here. 0:00 The interesting thing is that all the labels are right aligned and 0:04 fit the length of the label with the longest text. 0:07 Now, I've added a new Swift file to my project. 0:10 You can add one as well to an empty project. 0:14 And in here, 0:16 we have a bunch of starter code which you can grab from a link in the Notes section. 0:17 Let's walk through this. 0:22 So here, I've setup three labels and three textFields to populate our UI. 0:24 In each case, I'm using a closure that is immediately evaluated to group all 0:29 the setup logic in one place rather than spreading it out through viewDidLoad and 0:33 viewer layout subviews. 0:38 The only noteworthy lines of code in the setup stuff are the ones 0:39 setting the AutoresizingMasks to false, the 0:43 translateAutoresizingMaskIntoConstraints property. 0:47 With AutoresizingMasks set to true, which is the default in code, 0:50 we end up with system added constraints that can break our layouts. 0:54 So we have all six of them. 0:59 So we have a name label, nameTextField, email, and then password. 1:01 And then in viewWillLayoutSubviews, 1:04 we've gone ahead and added all of those to the view hierarchy. 1:06 We'll start by placing the first text field, the nameTextField, 1:11 at the top of the view. 1:14 Normally one thing we need to take into account is if our app is running iOS 11 1:16 and up, or versions prior to iOS 11. 1:20 This matters because of the addition of safe area layout guides in iOS 11, 1:23 which effects how we pin views to the surrounding layout guides. 1:27 Since the vertical position of this layout does not matter, we'll ignore this 1:32 consideration here, and just pin to the safe area layout guides directly. 1:37 But in practice you should always keep backward compatibility in mind, and 1:42 you would use the if available syntax to target specific platform versions and up. 1:45 So for iOS 11 and up, you have to say iOS 11. 1:50 Okay, to add our constraints, we're going to call the activate method on 1:54 the NSLayoutConstraint class that takes eight series, or an array of constraints. 1:58 The first constraint we're going to add is on the nameTextField. 2:04 And here, we'll create a constraint between the topAnchor of the text field. 2:07 So constraint equal to, and the top anchor of the view's safeAreaLayoutGuide. 2:12 We're going to give this a constant value of 16 points. 2:22 And next let's add a trailing space to the safe area constraint. 2:28 So from the nameTextField, we'll add a trailing constraint. 2:31 And again, this is equal to the view.safeAreaLayoutGuide and 2:38 from there we're going to hook up with the trailingAnchor. 2:42 And the constant value here is -16. 2:46 Remember that since the trailing edge of the nameTextField 2:49 is positioned to the left of the safeAreaLayoutGuide, 2:52 the constant value needs to be negative to denote direction. 2:55 Also since a text field contains content, make sure you use leading and 2:59 trailing anchors and not left and right anchors. 3:03 Oops, we forgot this here. 3:09 So this is .topAnchor. 3:10 There we go. 3:13 If you're build and run the project, you'll see that this is sufficient 3:14 information to position and size the text field. 3:17 So to actually build and run the project, since this is a custom view control sub 3:20 class in a field of many of these, we're going to go to main.storyboard. 3:24 We'll select the scene here. 3:28 And in the identity inspector, select the backup class, 3:30 the backing rather and set it form view controller. 3:34 When you do this, since main.storyboard is a storyboard that's automatically loaded. 3:37 By selecting the class that we're working with right now, 3:42 we can ensure that its contents are loaded. 3:44 Okay, so while that loads, let's go back to our code here at the bottom. 3:48 Okay, you can't actually see it because at the resolution I'm recording at, 3:53 it's quite zoomed out. 3:57 But it's going to be, there we go. 3:58 It's right there. 4:00 And you can see all the other stuff is just hovering in the top. 4:01 Now the reason that they're all over there, the labels and 4:05 text fields are just placed on top of each other in the top left, at origin. 4:07 Is because the starter code adds them to the subview, but 4:11 we haven't positioned them or sized them. 4:14 Now, this text field, if you can't see it, you can click on the debug view 4:17 hierarchy button to pause execution and capture the view hierarchy at this point. 4:24 And when you do, you can see it over here. 4:30 Okay, so we're gonna hit Play to go back. 4:33 Now previously, I stated that to correctly position and 4:36 size views, auto layout needs a minimum of four constraints, two in each axis. 4:39 Here we've only added two constraints total, and somehow auto layout 4:44 considers this view as correctly sized and positioned, we have no errors. 4:48 This is because a text field which contains content 4:52 has an intrinsic content sized and can provide a width and height on its own. 4:55 Implicitly, if we provide any constraints that affect the weight of the text field, 5:00 which we will in a second, we will override these implicit constraints 5:04 defined by the intrinsic content size and 5:08 auto layout will size the view as we have instructed. 5:10 All right, let's stop running this and go back to code. 5:15 On the left of the text field, we'll place the name label offset by 8 points. 5:18 So we'll say nameTextField.leadingAnchor.constraint 5:23 equalTo: nameLabel.trailingAnchor with a constant value of 8 points. 5:29 Now you could also flip this, it could originate from the name label's 5:36 trailing anchor to the name text field's leading anchor. 5:39 But at that point, the reason i did it this way, one is to show you, 5:43 it doesn't matter. 5:47 But two, to be aware that sign of the constant value whether it's positive or 5:48 negative depends on where the originating and the connecting view is. 5:54 Here the name text field is to the right of the name label. 5:58 And since the constraint originates from the name text field on the right, 6:01 we can put a positive constant value, since X values increase to the right. 6:07 Now if we flip this, if it went from the name label to the name text field, 6:12 this value would have to be negative. 6:16 Okay, so for the last one, let's add one constraint. 6:19 We have a trailing space constraint on the nameLabel, let's add one for 6:22 the leading space. 6:25 So we'll say nameLabel.leadingAnchor.constraint(equa- 6:26 lTo: view.safAreaLayoutGuide.leadingAnchor, 6:31 with a constant value of 8 points as well. 6:35 So this is a straightforward one, we're going to pin the leading anchor on 6:39 the name label to the leading anchor of the safe area layout guide, 6:42 with an offset of 8 points. 6:45 And this should sort out the horizontal position. 6:47 For the vertical position of the nameLabel, 6:51 we'll just align the first baseline of the nameLabel with the first baseline 6:53 of the name text field, so that the text rather than the views are aligned. 6:58 Alternatively the way we did this in Interface Builder was to 7:03 align the bottom edges of the name label and Textfield. 7:06 You could do either way. 7:09 So nameLabel, we don't particularly care about aesthetics in this recipe, 7:10 just about how to get to that final layout. 7:14 So nameLabel.firstBaselineAnchor.constraint 7:16 equalTo: nameTextField.firstBaselineAnchor and no constant value. 7:20 This also gives us a chance to use the baseline anchors, which we rarely ever do. 7:27 Okay, let's run this again. 7:32 And now our first label, TextField pair should be where we want it to be. 7:37 There we go. 7:42 For now at least. 7:43 We need to layout the rest of the views and the initial constraints we add here 7:44 for all of the remaining pairs are exact duplicates of the ones we just added. 7:48 So let's do some copy pasting here. 7:53 We'll add some comments so we can separate. 7:56 So this is Name and then this is going to be Email. 7:58 Right, so we'll copy all of this. 8:03 And then we need to carefully switch out which text fields and 8:05 labels we're talking about. 8:10 So emailTextField. 8:11 We're going to pin the topAnchor of the emailTextField, 8:15 not to the safeAreaLayoutGuide's topAnchor. 8:18 We're going to do this to the name TextFields.bottomAnchor and 8:20 the constant here is going to be 8 points. 8:26 Okay, again, we'll copy and paste that. 8:29 So the emailTextField'ss trailing anchor. 8:33 That's correct. 8:35 That goes to the safeAreaLayoutGuide's trailing anchor with the constant value 8:35 of -16. 8:39 Next for the emailTextField's leading anchor, that's going to go to 8:41 the emailLabel's trailing anchor for the constant value of 8 points. 8:45 Next up we have the emailLabel's leading anchor and that's going to the view's 8:52 safeAreaLayoutGuide's leading anchor, that's the same. 8:56 And then finally this is emailLabel. 8:59 First baselineAnchor to the emailTextField. 9:04 First baselineAnchor. 9:08 And we need a comma here. 9:09 Okay and then next let's add another comma and we'll say password. 9:11 And again we'll copy all of these. 9:17 And we'll paste it in. 9:23 Okay, so here it's passwordTextField, we'll copy that, 9:26 and this is going to go to the emailTextField's bottomAnchor. 9:32 And then passwordTextField's trailing anchor, that's same. 9:37 The passwordTextField's leading anchor is going to go to 9:41 the passwordLabel's trailing anchor. 9:45 The passwordLabel's leading anchor 9:49 is going to go to the safeAreaLayoutGuide's leadingAnchor. 9:53 And then the last one, just like before, 9:56 passwordLabel to passwordTextField with both the BaselineAnchors. 9:59 Okay, we've copypasted a bunch, so always good to run. 10:05 Make sure all our Labels and TextFields are in the positions we want them to be. 10:08 So the emailLabel and TextField are positioned where we want them, I think. 10:14 Where is, no, the passwordLabel, there's an issue. 10:18 And we probably did not name things correctly. 10:21 Let's see. 10:24 So passwordLabel.leadingAnchor. 10:26 Let's go back here. 10:30 Debug this quickly, so emailLabel where we want it to be 10:32 Why is the passwordLabel not there? 10:37 So passwordTextField.leadingAnchor.constraint 10:39 equalTo: passwordLabel.trailingAnchor. 10:43 leadingAnchor, that's correct. 10:46 Oops, this is incorrect. 10:49 So passwordLabel.firstBaselineAnchor, 10:50 you might have caught this while I was typing it, TextField, there we go. 10:52 Okay, let's run this. 10:56 So now the Labels and TextFields are where we want them to be. 11:00 But there seems to be a sizing issue with the TextFields. 11:04 Now, if you followed along with the Interface Builder version of this recipe, 11:07 you should have some idea of what's going on. 11:12 But there's a tricky aspect here. 11:14 The issue here has to do with the content hugging priority values. 11:17 Both the Llabel and the TextField have a content hugging priority value set to 11:21 medium or to 50 in the horizontal and vertical directions by default. 11:25 Remember that content hugging determines how snuggly the view fits around 11:30 its content. 11:35 The size of content is determined by the intrinsic content size of the view. 11:36 The label, having some content, is sized appropriately, and it looks fine. 11:41 But the text view, with no content, 11:46 the text fields, it has no intrinsic content size of its own. 11:48 So in this situation, 11:53 since the horizontal hugging effect makes the view really narrow. 11:54 And since the hugging priority values between the two views are identical, 11:58 auto layout cannot sensibly determine which one should be stretched 12:03 to accommodate our layout, and we end up with this weird scenario. 12:07 If you followed along with the interface builder version of this recipe, 12:11 you know that we didn't encounter this problem in that video. 12:15 This is because, and this is crucial, when using interface builder objects for 12:19 Labels and TextFields, the content hugging priority values for labels in both 12:23 the horizontal and vertical direction are one higher than those for the TextField. 12:29 In code, however, they have the same priority values. 12:34 Now I don't understand the reason for the inconsistency, to be honest. 12:39 In the notes I've included a link to a Stack Overflow post that lists out 12:43 all the default priority values for both content hugging and compression resistance 12:47 in both axes comparing interface builder objects and their views in code. 12:52 All right, so to fix the problem, we need to make the content hugging priority value 12:58 on the labels slightly higher than the values on the TextField. 13:02 We do this by calling the set content hugging priority method on a view. 13:07 In interface builder, we simply set priority values by specifying a number. 13:12 But in code, these are instances of the UI layout priority struct. 13:16 The struct contains static properties that define the system defaults of required 13:21 high, low and fitting level. 13:26 And the default value on these labels right now is 250 which is the default low. 13:29 We need to set that to 251 but we can't just pass in any integer value. 13:35 Instead, we need to wrap it in an initializer defined on the struct. 13:42 So before activating any of these constraints and 13:46 between the adding of the sub views, we'll go ahead and set these values. 13:49 So we'll say nameLabel.setContentHuggingPriority. 13:54 So l'll say UILayoutPriority and 13:58 in parenthesis I'll say 251 for the horizontal direction. 14:01 So we need to make sure we're doing this for all the labels. 14:08 So we'll say emailLabel.setContentHuggingPriority(UILa- 14:11 youtPriority(251), for: .horizontal) direction. 14:16 And then we'll do that again for the passwordLabel. 14:20 Now if you run the app, autolayout stretches the second text fill to fill up 14:27 the empty space then the third as well, and 14:31 it satisfies our initial set of constraints. 14:34 Okay, so now we're in a situation where we've gotten the label text field pair 14:37 up on the screen, but the textFields are all different widths. 14:42 So we have a layout, but it doesn't match the layout we desire. 14:46 Let's add in two more constraints. 14:51 Okay, so at the bottom of our array we'll say 14:53 nameTextField.widthAnchor.constraint(equa- lTo: 14:57 emailTextField.widthAnchor, with a multiplier of 1.0. 15:03 And then again we'll say emailTextField.widthAnchor.constraint(equ- 15:10 alTo: ) Password text field add width anchor with a multiplier of 1.0. 15:16 Now if we run the app, somehow we've arrived at the desired layout. 15:24 Well, almost there. 15:30 We have one change to make, 15:31 which we'll do right now before kind of walking through the explanation. 15:32 So for each of these labels, Let's 15:36 do label.alignment, textAlignment rather, 15:41 = .right, and we'll paste that for each one. 15:46 Because remember, we do want those labels to be right aligned. 15:52 Oops, not in there, passwordLabel. 15:56 So that our final layout matches the layout that we originally set 16:02 out to build. 16:04 Cool. 16:07 Okay, so what just happened here? 16:08 This seems an intuitive but again, 16:10 this final layout has to do primarily with content hugging priorities. 16:12 It's important to understand what happened here and why, so 16:18 we don't expect this behavior automatically. 16:20 When we sent the content hugging priorities on the labels 16:23 to be one higher than the text fields, this made all the difference. 16:26 Remember that content hugging is a view fighting the urge to grow 16:31 with the view fitting snugly around the content. 16:35 Within a given row between the label and the text field. 16:38 Since the label has a higher content hugging priority value, 16:41 auto layout is not going to stretch the label when deciding on a width. 16:45 The label will be sized to fit its content and then the text field will occupy any 16:49 of the remaining horizontal space after taking spacing constraints into account. 16:54 Because that can be stretched relative to the label. 16:59 So this is the first piece of our puzzle. 17:02 In each row, auto layout ensures that the label is sized so that the content fits. 17:05 The last piece of our puzzle is adding those equal width constraints to all 17:10 the text views. 17:14 When we do this, auto layout needs to decide what width to set all of them to. 17:16 Remember, the three of them had different widths when we started that or 17:20 when we were at that intermediate point. 17:24 So if at that point, 17:27 auto layout had chosen the width of the first text field, then we would have 17:28 an issue because the nameTextField was the second longest of the three. 17:32 In the last row, the passwordLabel was sized to show all the text. 17:38 And since it has a higher content hugging priority relative to its TextField, 17:42 when setting that equal width constraint, 17:46 auto layout would have had to grow that text field but it won't shrink it. 17:49 It won't shrink the password field to allow the TextField to grow and 17:54 have the same width as the first TextField. 17:57 The only logical solution is for auto layout to find the text field with 18:00 the lowest width value and set that as the width of all the text fields. 18:04 This way, it can ensure that none of the labels will have their content clipped or 18:09 truncated. 18:13 And that satisfies the content hugging and 18:15 content compression resistance values of the labels relative to the text fields. 18:17 By doing this, we've defined a layout where we can ensure that 18:22 all the labels will be sized to the width of the longest label. 18:25 Then, by right aligning the labels, we've also ensured that 18:30 all the edges are aligned despite having labels of different widths. 18:33 If you're interested in implementing this layout in Interface Builder, 18:37 check the notes section for a link. 18:40
You need to sign up for Treehouse in order to download course files.Sign up