4.2 Layer-Based Controls
In this lesson you'll learn that layers are the best way to completely customize the appearance of your interface.
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.2 Layer-Based Controls
Hi, and welcome back to Go Further with Swift, where we build a weather app for iOS. In this lesson, I'm going to show you how you can create a completely custom control with layers. Let's have look at the current state of the project. In the last lesson, we removed the condition, image view, and label, and merged them into a composite view. Now, I want to create a view that shows the current wind direction within a compass-like display. I'm going to create a new class called WindDirectionView for it. I'm going to make it a subclass of UI control again. It doesn't make that much difference if choose UI view here or not. Let's also make it IBDesignable. Since we want to do some drawing on the layer, I'm going to create an accompanying class called WindDirectionLayer, a subclass of CALayer. Of course, we need our few initializers again, init frame and init coder, both calling their parent function. I'm also going to create a sharedInitialization function that gets called by both initializers. Now that the bar operate code is set up, let's get going with the layers. First, we need a new instance of the layer which we then add to the view's default layer as a sublayer. I also want to store our presentation of this windLayer in the instance. But of course, just layer isn't going to work because that's taken. Now that the layer is hooked up to the view, let's customize it. My idea of the wind direction is a circle around the pointer that looks like a compass. Like the one that is shown when an app uses location services. I'm going to create multiple layers that build this final display. The first one, I call border layer which is a CAShapeLayer. There are a few layer types you can use in QuartzCore. The simple ones are a gradient layer, that draw various forms of gradients with linear or radial. The text layer for entering strings, and finally the shape layer where you can draw a path that gets drawn. You can also have particle emitters, replicators, tiles and other types that are beyond the scope of this lesson. So let's lazily initialize such a shape layer. I always write to return immediately, so I don't forget. Xcode would complain anyway but it's just a nice habit to have. First, we have to set the stroke color on our layer. I'm going to use white. When working with layers, you don't use UIKit classes. We are in CG land here, that's why we have to convert the white color into a CGColor. Next is the lineWidth for the circle, 10 seems to be a good value. Since I only want to draw a ring, I'm setting the fill color to nil. So the circle we're about to draw doesn't get filled. That's all for now, I don't want to draw within the initializer for various reasons. I'll get to them when it's time to draw. The second layer is the directionLayer. It's the same procedure like before. This time, we're setting the lineWidth to 0 and the fill color to white as well. Setting the stroke color isn't really necessary, it's just another habit. We start with two layers, we'll also need a wind direction to point to. I'm going to make this a float that can range between 0 and 360, it's just easier that way. As always I can't just use one initializer. We use the default one above, but when it comes to storing and restoring, like with Interface Builder, init coder will always get called. That's why we need a sharedInitialization function here, as well. Within this function, I'm going to add the two layers and sublayers. We also have to override this layer and sublayers method. Because those layers are just images that have been drawn for a provided size. If that size changes, we have to change the bounds and redraw, otherwise, the layers would look squashed or stretched. I'm going to check if the bounds of the WindDirectionLayer are the same as our bounds layer. If they aren't, I know that there has been a resize. And I will set the bounds and position for our two shape layers. The position will be set to the center point, which can be calculated by taking the width and height of the bounds, and dividing both of them by 2. To issue the refresh, I'm calling preparePaths. Okay, now it's time to draw something. First, let's create a Bezier path on a border layer. UIBezierPath has an initializer that takes the center of an arc, a radius, as well as the start and end angle in radians. Our center is the position and the radius is going to be width minus 10, which is the line width divided by 2. I'm only subtracting the line width once, since the line stroke will be drawn centered on the circle's path, five points inside and five points outside. Since we want a full circle I'm going to start at 0 and end at 2 pi, which is a full rotation in radians. You can use CGFloat.pi to reference it. In earlier versions of Swift, you had to use m_pi, but in Swift 3, it is a constant directly under types. Clockwise or not, doesn't really matter in our case. But we will need a CGPath again. The second path for the directionLayer, will be a mutable path. Since I need to draw single lines to form it step by step. First, I move to a point that forms the lower left, I'm trying to be as relative with my measurements as possible. So I'm moving at 1/10th of the width from the middle in the x direction, and 3/4 from the y origin. I could have used height here but the view needs to be square anyways, so it doesn't matter. While move is like lifting up the brush and moving it to a new position when painting, addLine does the same thing with the brush on the canvas. This will be our first line. Directly on the center and a quarter of the width from the origin. The other two corners will have similar coordinates. The third one is the mirrored version of the first corner, and the fourth one will be on the center like the second point, just a bit shifted. Let's also call preparePaths, when the wind direction was changed. Then I'm going to go to Interface Builder and add a new UI view. I'm going to wedge it between the city label and the condition view, vertically aligned and also enforce an aspect ratio of one to one. Then, let's set the class to our WindDirectionView. Nothing gets drawn yet, but I'm setting Xcode up, so I can see the interface on the left while I work on the view. So, first off, we need to set the bounds and position of the windLayer to the default layer's bound and its center. Then we can override the layoutSublayers of layer function, To also set the bounds and center here. And then call layoutSublayers on the windLayer. After Xcode compiles this, our WindDirectionView shows up on the left. And it's looking good already. The only thing missing is the correct direction. To pass the wind direction from Interface Builder to the layer, I need to pass it through the view. This can either be done with a separate property under view and the didSet callback. Or even better by using set and gather functions that act like a pass through. After I change the generic CALayer class of windLayer to WindDirectionLayer, Everything works. We can even make this property work in Interface Builder. Of course we haven't implemented the rotational part yet. So let's return to the layer class and add a transform to the directionLayer. I'm going to use CATransform3DMakeRotation, where I transform direction to radians, and only use the C axis. And with that, we're done. The layer is rotating and when I change it in Interface Builder, it updates correctly. The final thing to do is to hook it up in our ViewController and set some real values from the API. The value we're looking for is under the wind key, and then the deg key. Let's build and run and see our work in action. If you remember the first lesson about constraint based animation, this is a case where it might not be what you want. Since we depend on the position of the city label, the windDirectionView is in the wrong place until the label appears, but it's already in the target size. In the next lesson, we are going to create an interactive drawing canvas with Core Graphics. See you there.