
This video is only available to subscribers. Start a subscription today to get access to this and 484 other videos.
Custom CALayer
This episode is part of a series: Dive Into Core Graphics.
1. Intro 2 min |
2. Basic Shapes 7 min |
3. Paths 17 min |
4. Colors 9 min |
5. Gradients 11 min |
6. Clipping Paths 6 min |
7. Context Transforms 10 min |
8. Images 7 min |
9. Text 9 min |
10. Offscreen Rendering 12 min |
11. Custom CALayer 13 min |
12. Pie Progress View 7 min |
13. Watermarking Photos 6 min |
14. Working in AppKit 8 min |
Links
## Setting Up the Playground with a View Controller
We want some interactivity this time, so we’ll use a view controller.
import UIKit
import PlaygroundSupport
final class ProgressView: UIView {
var progress: CGFloat = 0
}
final class ViewController: UIViewController {
let progressView = ProgressView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
progressView.progress = 0.31
progressView.backgroundColor = .clear
progressView.frame = view.bounds
progressView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(progressView)
}
}
PlaygroundPage.current.liveView = ViewController()
Here we have a custom view that has a progressView as a subview. We'll tap on the main view to increment the progress of the progress view.
// in viewDidLoad...
let tap = UITapGestureRecognizer(target: self, action: #selector(increment))
view.addGestureRecognizer(tap)
And then we can use that handler to increment a new property:
@objc private func increment() {
progressView.progress += 0.1
}
Creating a Custom Layer-backed View
We want to do the drawing with a custom CALayer, so we'll create that first:
private final class ProgressLayer: CALayer {
var progress: CGFloat = 0 {
didSet { setNeedsDisplay() }
}
private let fillColor = UIColor.blue.cgColor
override init() {
super.init()
}
override init(layer: Any) {
super.init(layer: layer)
guard let progressLayer = layer as? ProgressLayer else { return }
progress = progressLayer.progress
}
override func draw(in context: CGContext) {
context.setFillColor(fillColor)
var rect = bounds
rect.size.width *= progress
context.fill(rect)
}
required init?(coder aDecoder: NSCoder) {
fatalError()
}
}
We want to use this layer as our view's layer, and also forward any progress changes over to the layer.
var progress: CGFloat {
get {
return progressLayer?.progress ?? 0
}
set {
progressLayer?.progress = newValue
}
}
override class var layerClass: AnyClass {
return ProgressLayer.self
}
private var progressLayer: ProgressLayer? {
return layer as? ProgressLayer
}
Supporting Animations
In order to support animations, we'll have to make a few changes. First, our property needs to by marked as @NSManaged
:
class ProgressLayer : CALayer {
@NSManaged var progress: CGFloat
...
}
Then we need to tell the system that when the progress changes, we need to redraw:
override class func needsDisplay(forKey key: String) -> Bool {
if key == "progress" {
return true
}
return super.needsDisplay(forKey: key)
}
And we need return an CAAction
for when this key changes, which will likely be an animation:
override func action(forKey key: String) -> CAAction? {
if key == "progress" {
let animation = CABasicAnimation(keyPath: key)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fromValue = presentation()?.value(forKey: key)
return animation
}
return super.action(forKey: key)
}
Note that we use the presentation()
instead of the progress on self
directly. CALayers have a presentation layer which hold the current values that are being displayed, whereas the layer just contains the starting/ending values for any animation.
Consider for instance moving a box’s X position from 0
to 100
. The layer would get this value set to 100 immediately, but the presentation layer would have its value reflect the in between animation state. By doing this we can ensure our animation starts from wherever it currently is on screen, rather than resetting it back to the start position.
We’ll have to use this presentation value as well when we are drawing:
let progress = presentation()?.progress ?? 0
var rect = bounds
rect.size.width *= progress
context.fill(rect)