CALayer
has internal/private support for two pretty cool things: archives and states. We'll talk about states first, as they're the more complex part of the exercise.
A CALayer
can have any number of states
which are CAState
objects containing a number of CAStateElement
s. The elements configure the state, and can target
any sublayer of the root layer, and any keyPath
on CALayer
. It looks like it can also have a source
(which is a CAStateElement
itself) but I'm not sure what that specifically means. A state can also be basedOn
another CAState
, presumably one that preceeds it in the state diagram of the layer; an initial
state is one that a layer can start off with (and doesn't need a name
to uniquely identify it). On top of that, a CALayer
may also have a number of stateTransitions
which are CAStateTransition
objects (containing CAStateTransitionElement
s). The typical use case will be to create elements with a target
and an animation
representing the keyPath
that needs to be animated between the two states. Be sure to set the toState
and fromState
on your transition to match the unique state name
s from earlier. If you use the string "*"
, I believe it refers to any possible state. Here's a code sample:
let s1 = CAState()
s1.name = "inactive"
s1.isInitial = true
let e1 = CAStateSetValue()
e1.keyPath = "backgroundColor"
e1.value = NSColor.red.cgColor
e1.target = layer
s1.addElement(e1)
let s2 = CAState()
s2.name = "active"
let e2 = CAStateSetValue()
e2.keyPath = "backgroundColor"
e2.value = NSColor.blue.cgColor
e2.target = layer
s2.addElement(e2)
let t1 = CAStateTransition()
t1.fromState = "*"
t1.toState = "active"
let e3 = CAStateTransitionElement()
e3.key = "backgroundColor"
e3.animation = CABasicAnimation(keyPath: "backgroundColor")
e3.target = layer
e3.isEnabled = true
e3.duration = 2.0
t1.elements = [e3]
let t2 = CAStateTransition()
t1.fromState = "*"
t1.toState = "inactive"
let e4 = CAStateTransitionElement()
e4.key = "backgroundColor"
e4.animation = CABasicAnimation(keyPath: "backgroundColor")
e4.target = layer
e4.isEnabled = true
e4.duration = 2.0
t2.elements = [e4]
let t3 = CAStateTransition()
t3.fromState = "inactive"
t3.toState = "active"
let e5 = CAStateTransitionElement()
e5.key = "backgroundColor"
e5.animation = CABasicAnimation(keyPath: "backgroundColor")
e5.target = layer
e5.isEnabled = true
e5.duration = 2.0
t3.elements = [e5]
let t4 = CAStateTransition()
t4.fromState = "active"
t4.toState = "inactive"
let e6 = CAStateTransitionElement()
e6.key = "backgroundColor"
e6.animation = CABasicAnimation(keyPath: "backgroundColor")
e6.target = layer
e6.isEnabled = true
e6.duration = 2.0
t4.elements = [e6]
layer.states = [s1, s2]
layer.stateTransitions = [t1, t2, t3, t4]
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
self.ctrl = CAStateController(layer: layer)!
self.ctrl.setInitialStatesOfLayer(layer, transitionSpeed: 0.5)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
self.ctrl.setState(s2, ofLayer: layer, transitionSpeed: 0.5)
}
}
To actually apply and modify states on a layer, you'll want to create a CAStateController
. It's simple to use, as you initially call setInitialStatesOfLayer(_:)
and to change a state, call setState(ofLayer:)
. If you include the transitionSpeed
argument, it'll set a duration for any possible transitions to occur (independently of the duration of its element
s).
I don't recommend using CAStateController
and CAState
without having written a good CoreAnimation "Interface Builder" to help you design states and layers correctly. Since Apple likely has this tool and we don't, it's probably why the API is private at the moment. A first step towards this, however, is to use CAML
packages correctly. There is a CAPackage
class to aid in all of this, but we'll use CAMLWriter
and CAMLParser
instead, which mirror NSKeyedArchiver
and NSKeyedUnarchiver
, but serialize into a "core animation archive" (caar) which uses a different XML format. Here's a code sample:
// Write:
let data = NSMutableData()
let writer = CAMLWriter(data: data)
writer?.encode(layer)
// Read:
let parser = CAMLParser()
_ = parser.parseData(data as Data)
let layer2 = parser.result as! CALayer
Pretty simple right!? Now the challenge is to build an interface designer to model CALayer
s and CAState
s. Encode it into a caml
file and decode it at runtime, and you're all set. Use a CAPackage
to neatly wrap the loading logic -- check out AVMicaPackage
in AVKit, some of the packages in DictationServices (within PrivateFrameworks/SpeechObjects), or PassKitUIFoundation.
Regarding CAState
, I'm still not sure what CAState.locked
, CAStateElement.source
, CAStateControllerLayer
(which isn't even a CALayer
subclass??) or CAStateControllerUndo
(and related undoStack
) do. I'll investigate those but if anyone has any idea, drop me a line and let me know!