4.1 Basic Controls
The simplest form of a custom control is a composite view of
UIKit components. We are going to create a composite view for the weather condition in this lesson.
1.Introduction3 lessons, 12:12
2.Animation3 lessons, 21:28
3.Networking3 lessons, 23:55
4.Custom Controls3 lessons, 32:22
5.Conclusion1 lesson, 01:44
4.1 Basic Controls
Hi, and welcome back to Go Further with Swift where we build a weather app for iOS. In this lesson, we will create our first custom control that encompasses one that consists of multiple regular UIView controls. What I want to do is to take my two condition views, the image and the label, and move it into its own view. It updates both after setting the weather condition on it. So, let's create a new file and call it ConditionView. We need to import UIKit instead of Foundation and then create a class that is a subclass of UIView. There are two important intializers on a view, the first one is init frame and the second one is init coder. Now, I have to create my views. I'm going to do it as lazy initialized variables. This allows us to put the set up code into its own function, but that gets executed when we first use the ImageView. So, the most important property in the ImageView is to set translateAutoresizingMaskIntoConstraints to false. Apple still didn't change the default of it, which I don't quite understand, since most of the time you will want to set this to false anyways. Then, I'm also setting the contentMode to scaleAspectFit. The second view is going to be the label. We're doing the same thing here with the lazy variable. Of course, the properties are going to be mostly different. I'm setting the font to size 36 regular weight font. Then, the textColor to white, the alignment to center. And the shadow to be black and with an offset of 1 and 1. Since we have two initializers that can be run, we have to create a shared set up function to configure the views. Here, again, we have to stop AutoresizingMask to interfere with our constraints and then add the subviews. This will use the code we just wrote to configure them. Now, we can call initializeView and the two initializers after calling super. Right now, the views don't have any other layout constraints. We have to set them ourselves after we added the subviews to the superview. The first one is the leading anchor, which constrains to the parent view's leading anchor, meaning the left side on the form that has the left to right setting. If you have set your phone to use a right-to-left language like Arabic, this would be on the right side. Very handy. The other constraints positioning the imageView are the ones for top and trailing. Since the imageView should be a square, I can set the width anchor to equal it's height anchor. While I'm creating the constraints, I have to say that the constraint methods for anchors, it has been added in iOS 9 is very convenient and handy. And a large shoulder than the previous NSLayoutConstraint, constraint with visual format method. The visual format was a nice attempt, but personally, I like to just list my constraints in a compact way like we can do here. So, we have set all the constraints, adding some between the imageView and the label, as well as between the label and the parent view. Now, that this is done let's do some clean up in the ViewController. We can get rid of all references to the ImageView and the label we had previously. Xcode starts to complain about the imageView's image but I'm going to leave it right there. So we can copy and paste it later on. Let's add a new variable that holds the view. In viewDidLoad, I can initialize and add it to the parent view. Now, for some constraints. I'm going to just add it to the center by using centerXAnchor and centerYAnchor. To specify the width, I'm also going to set the leading anchor constraint with a constant of 60. Meaning, it's going to have some spacing from the screen's border. In the storyboard, we have to also remove the views as well as the Stack View. Of course, since the conditionView isn't optional, it has to be unwrapped. The easiest way is to unwrap it in an if condition. I also need to comment out the switch statement where we set the image. Since there isn't an imageView and the label anymore, the animations need to be updated. We can add them to the new condition view. Now, that the view is configured correctly, let's make it possible to set a weather condition. In the ConditionView, I'm going to add a property called condition of type, WeatherCondition, and already set it to other with a detail of fair. WeatherCondition is an enum. It defines the different categories with a detail string every time. It isn't the most sophisticated way of doing this but it will work for us. To update the views, after the condition has been set, I'm going to add a didSet call back to the condition property. In here, I can use a switch statement to set the appropriate image and label. Unfortunately, it isn't quite possible to set the label in a more elegant fashion since this is how enums work. Back in the ViewController, I can now uncommand the auto switch label and set the conditionView's condition to be of the correct category and using the weather description as a detail. I also forgot to add the clouds condition which is from 801 to 809. So let's add that. For the first time now, let's build and run. It has the condition icon and the detail, which is, again, haze. But it doesn't quite look right. When creating your constraints, you have to be aware of the correct order. Meaning, it is different if you constraint the bottom anchor to the top anchor or vice versa. You can use a constant of minus 8 instead. But it is much cleaner if you just swap the two around. I also didn't capitalize the label's text. Much better now. Now, that we've created this custom view, wouldn't it be nice if we could do the layout with did in the view controller in Interface Builder instead to get a better sense of how it's going to look when designing it? And we can. Let's remove the code in viewDidLoad and switch to an IB outlet. Because of this change, we can also remove the question marks when setting the condition. In Interface Builder, I can add a generic UI view to the scene and constrain it like before but getting instant visual feedback. Now, we have to set the type of it to be a Condition View. Interface Builder complains about not having the correct vertical position. Since it doesn't know about the constraints, we set in the Condition View class itself. This can be fixed by making our custom view to be compatible with Interface Builder, By setting the IB design-able marker. Something to mention, although I'm not going to implement this for the ConditionView, you want to use UIControl instead of UIView whenever you want to make the view interactive like a button. Have a look in the documentation for that. As an exercise, you could implement a feature, it cycles through the different conditions that are provided by the API whenever the user taps on the ConditionView. Back in Interface Builder, you can now see that it has the correct size. Since we can create it like a normal view, I can set the background from being white to being transparent. Another idea would be that you want to make the spacing between the label and the image view adjustable in Interface Builder. We can easily do that. Let's create a new property called spacing and make it a CGFloat. By adding the IBInspectable marker, we'll be able to set it in Interface Builder. To make it work, we need a didSet call back and also a new property that stores the spacingConstraint. In the callback, we just need to adjust the constant of the constraint to our spacing property. Beside the spacingConstraint, we also have to change the initializer a bit. Pulling out the constraint creation and setting it to a variable beforehand. When we look at Interface Builder, you can see there is a new property called Spacing. When I set it to value like 20, you can also see that the frame in the scene is now outdated since the view that change its intrinsic size. To wrap up this lesson, I'm also going to introduce you to the prepare for Interface Builder function. If you want to create some dummy content, that gets displayed in Interface Builder, for instance because you get your data from the network like reader, you can set it up here. I'm going to set the condition to be after drizzle group with the name Sample Condition. When I open the storyboard again you can see that Xcode is compiling something and changing the view. Unfortunately, sometimes Interface Builder fails to render your custom views correctly as it does in this case. When I move the ConditionView and reset the frames, the label moves to a position closer to the view. But that still isn't correct. This was the first way of creating a custom view. In the next lesson, we are going to take a look at layers and how we can draw custom elements. See you there.