IOS animation is mostly UIView, some complex need to use the core animation, but completely different style of use, and complex call process is really cute new headache.
A few days ago with the need to do animation, with Swift to expand the core animation library, with much more comfortable.
Don’t blow the first look at the code:
view.layer.animate(forKey: "cornerRadius") {
$0.cornerRadius
.value(from: 0, to: cornerValue, duration: animDuration)
$0.size
.value(from: startRect.size, to: endRect.size, duration: animDuration)
$0.position
.value(from: startRect.center, to: endRect.center, duration: animDuration)
$0.shadowopacity. Value (from: 0, to: 0.8, duration: animDuration)$0.shadowColor
.value(from: .blackClean, to: color, duration: animDuration)
$0.timingFunction(.easeOut).onStoped {
[weak self] (finished:Bool) in
iffinished { self? .updateAnimations() } } }Copy the code
The above code animates a view’s rounded corners, dimensions, positions, shadows, and shadow colors, sets the change mode to easeOut, and calls another method when the whole animation is over
shareLayer.animate(forKey: "state") {
$0.strokeStart
.value(from: 0, to: 1, duration: 1).delay(0.5)
$0.strokeEnd
.value(from: 0, to: 1, duration: 1)
$0.timingFunction(.easeInOut)
$0.repeat(count: .greatestFiniteMagnitude)
}
Copy the code
A circular progress bar animation with the shape CAShareLayer looks like this
So how does this work?
First of all, it’s definitely extending CALayer, Add the animate method, where the closure passes the user an AnimationsMaker animation constructor generic to the actual type of the current CALayer (since Layer could be CATextLayer, CAShareLayer, CAGradientLayer … Wait, they all inherited from CALayer.)
This way we can precisely add properties that can be animated to the constructor, properties that can’t be animated to the constructor. Don’t come out.
extension CALayer {
public func animate(forKey key:String? = nil, by makerFunc:(AnimationsMaker<Self>) -> Void) {
}
}
Copy the code
The idea was good, but unfortunately it failed.
Xcode says Self can only be used as a return value or in a protocol. Isn’t there a solution?
The answer is yes
CALayer inherits from the CAMediaTiming protocol, so we simply extend this protocol and add the condition that we must inherit from CALayer, which works just as well as extending CALayer directly.
extension CAMediaTiming where Self : CALayer {
public func animate(forKey key:String? = nil, by makerFunc:(AnimationsMaker<Self>) -> Void) {
}
}
Copy the code
OK works fine, complete success, but what if a class doesn’t implement the XXX protocol? Does it still work?
The answer is yes
Write an empty protocol, extend the target class to implement it, and then extend the empty protocol, provided that you inherit from that class, and then add methods.
The next step is to create the animation constructor
open class AnimationsMaker<Layer> : AnimationBasic<CAAnimationGroup, CGFloat> where Layer : CALayer {
public let layer:Layer
public init(layer:Layer) {
self.layer = layer
super.init(CAAnimationGroup())
}
internal var animations:[CAAnimation] = []
open func append(_ animation:CAAnimation) {
animations.append(animation)
}
internal var _duration:CFTimeInterval?
/* The basic duration of the object. Defaults to 0. */
@discardableResult
open func duration(_ value:CFTimeInterval) -> Self {
_duration = value
return self
}
}
Copy the code
The obvious goal is to create a group of core animations that can be easily combined into one
Let’s start refining the previous method
extension CAMediaTiming whereSelf: CALayer {public func animate(forKey key:String? = nil, by makerFunc:(AnimationsMaker<Self>) -> Void) {// remove the animation with the same keyif let idefiniter = key {
removeAnimation(forKey: idefiniter)} // Create the animation constructor and start constructing the animationletMaker = AnimationsMaker<Self>(Layer: Self) makerFunc(maker) // If only one attribute is animated, the animation group is ignoredif maker.animations.count == 1 {
returnadd(maker.animations.first! .for} // Create an animation groupletGroup = maker.caanimation group. Animations = maker.animations // If no animation time is set, Group. duration = maker._duration?? maker.animations.reduce(0) { max($0.The $1.duration + The $1.beginTime)} // Start animation add(group,forKey: key)
}
}
Copy the code
The natural next step is to add CALayer’s various animatable properties to the animation constructor
The extension AnimationsMaker {/ / / the cornerRadius property to the default animation 0 public var cornerRadius: AnimationMaker < Layer, CGFloat > {return AnimationMaker<Layer, CGFloat>(maker:self, keyPath:"cornerRadius"Public var bounds:AnimationMaker<Layer, CGRect> {return AnimationMaker<Layer, CGRect>(maker:self, keyPath:"bounds"Public var size:AnimationMaker<Layer, CGSize> {return AnimationMaker<Layer, CGSize>(maker:self, keyPath:"bounds.size"} /// The following attributes are omitted...... }Copy the code
AnimationMaker here is similar to the previous AnimationsMaker, but in the sense of a single-attribute animation constructor
The fromValue and toValue properties in CABasicAnimation are all Any?
The reason is that when animating different attributes of layer, the value type given is also uncertain. For example, the size property is CGSize, position property is CGPoint, zPosition property is CGFloat, etc. Therefore, it can only be Any? .
However, this does not meet the goal of Swift security language, because when we use it, we may accidentally pass an incorrect type to it without being detected by the compiler, which increases the time of debugging and is not conducive to production efficiency
Therefore, when defining AnimationMaker(single-attribute animation), use generic constraints to change the value of the AnimationsMaker to the same type as the animation property value, and pass AnimationsMaker to the animation group to facilitate the addition of its own constructed CAAnimation
public class AnimationMaker<Layer, Value> where Layer : CALayer {
public unowned let maker:AnimationsMaker<Layer>
public letkeyPath:String public init(maker:AnimationsMaker<Layer>, KeyPath :String) {self.maker = maker self.keyPath = keyPath} *) func animate(duration:TimeInterval, damping:CGFloat, from begin:Any? , to over:Any?) -> Animation<CASpringAnimation, Value> {let anim = CASpringAnimation(keyPath: keyPath)
anim.damping = damping
anim.fromValue = begin
anim.toValue = over
anim.duration = duration
maker.append(anim)
returnAnimation<CASpringAnimation, Value>(anim)} func animate(duration:TimeInterval, from begin:Any? , to over:Any?) -> Animation<CABasicAnimation, Value> {let anim = CABasicAnimation(keyPath: keyPath)
anim.fromValue = begin
anim.toValue = over
anim.duration = duration
maker.append(anim)
returnAnimation<CABasicAnimation, Value>(anim)} values:[Value]) -> Animation<CAKeyframeAnimation, Value> {let anim = CAKeyframeAnimation(keyPath: keyPath)
anim.values = values
anim.duration = duration
maker.append(anim)
returnAnimation<CAKeyframeAnimation, Value>(anim)} path:CGPath) -> Animation<CAKeyframeAnimation, Value> {let anim = CAKeyframeAnimation(keyPath: keyPath)
anim.path = path
anim.duration = duration
maker.append(anim)
return Animation<CAKeyframeAnimation, Value>(anim)
}
}
Copy the code
To avoid possible loop-reference memory leaks, set parent animation group maker to unowned (equivalent to OC’s assign) without increasing the reference count.
Although there are no circular references, since these are temporary variables, there is no need to increase the reference count, which can make the operation more efficient
AnimationMaker only gives the necessary basic properties to the Animation. Some additional properties can be set via chain syntax, so it returns an Animation object that wraps the CAAnimation and also passes a generic value type
public final class Animation<T, Value> : AnimationBasic<T, Value> where T : CAAnimation {
/* The basic duration of the object. Defaults to 0. */
@discardableResult
public func duration(_ value:CFTimeInterval) -> Self {
caAnimation.duration = value
return self
}
}
Copy the code
Since both CAAnimation and CAAnimationGroup have some properties in common, we created a base class AnimationBasic and added extra time to the animation group. By default, the maximum time of all animations is used when this is not given. Otherwise, a mandatory time is used. Refer to the AnimationsMaker definition above
open class AnimationBasic<T, Value> where T : CAAnimation {
open let caAnimation:T
public init(_ caAnimation:T) {
self.caAnimation = caAnimation
}
/* The begin time of the object, in relation to its parent object, if
* applicable. Defaults to 0. */
@discardableResult
public func delay(_ value:TimeInterval) -> Self {
caAnimation.beginTime = value
return self
}
/* A timing function defining the pacing of the animation. Defaults to
* nil indicating linear pacing. */
@discardableResult
open func timingFunction(_ value:CAMediaTimingFunction) -> Self {
caAnimation.timingFunction = value
return self
}
/* When true. the animation is removed from the render tree once its * active duration has passed. Defaults to YES. */ @discardableResult open func removedOnCompletion(_ value:Bool) -> Self { caAnimation.isRemovedOnCompletion = valuereturn self
}
@discardableResult
open func onStoped(_ completion: @escaping @convention(block) (Bool) -> Void) -> Self {
if let delegate = caAnimation.delegate as? AnimationDelegate {
delegate.onStoped = completion
} else {
caAnimation.delegate = AnimationDelegate(completion)
}
return self
}
@discardableResult
open func onDidStart(_ started: @escaping @convention(block) () -> Void) -> Self {
if let delegate = caAnimation.delegate as? AnimationDelegate {
delegate.onDidStart = started
} else {
caAnimation.delegate = AnimationDelegate(started)
}
return self
}
/* The rate of the layer. Used to scale parent time to local time, e.g.
* if rate is 2, local time progresses twice as fast as parent time.
* Defaults to 1. */
@discardableResult
open func speed(_ value:Float) -> Self {
caAnimation.speed = value
return self
}
/* Additional offset in active local time. i.e. to convert from parent
* time tp to active local time t: t = (tp - begin) * speed + offset.
* One use of this is to "pause" a layer by setting `speed' to zero and * `offset' to a suitable value. Defaults to 0. */
@discardableResult
open func time(offset:CFTimeInterval) -> Self {
caAnimation.timeOffset = offset
return self
}
/* The repeat count of the object. May be fractional. Defaults to 0. */
@discardableResult
open func `repeat`(count:Float) -> Self {
caAnimation.repeatCount = count
return self
}
/* The repeat duration of the object. Defaults to 0. */
@discardableResult
open func `repeat`(duration:CFTimeInterval) -> Self {
caAnimation.repeatDuration = duration
return self
}
/* When true. the object plays backwards after playing forwards. Defaults * to NO. */ @discardableResult open func autoreverses(_ value:Bool) -> Self { caAnimation.autoreverses = valuereturnself } /* Defines how the timed object behaves outside its active duration. * Local time may be clamped to either end of the active duration, or * the element may be removed from the presentation. The legal values * are `backwards', `forwards', `both' and `removed'. Defaults to
* `removed'. */ @discardableResult open func fill(mode:AnimationFillMode) -> Self { caAnimation.fillMode = mode.rawValue return self } }Copy the code
Let’s add the icing on the cake by adding quick creation to a single property animation
Extension AnimationMaker @discardAbleresult public func values(_ values:[Value], duration:TimeInterval) -> Animation<CAKeyframeAnimation, Value> {returnanimate(duration: duration, values: Values)} // create an elastic animation from begin to over and execute duration seconds @available(iOS 9.0, *) @discardableResult public func value(from begin:Value, to over:Value, damping:CGFloat, duration:TimeInterval) -> Animation<CASpringAnimation, Value> {returnanimate(duration: duration, damping:damping, from: begin, to: @discardAbleresult public func value(from begin: value, to over: value, duration:TimeInterval) -> Animation<CABasicAnimation, Value> {returnanimate(duration: duration, from: begin, to: @discardAbleresult public func value(to over: value,)} /// create an animation from the currently animated value to update to over and execute duration seconds duration:TimeInterval) -> Animation<CABasicAnimation, Value> {letbegin = maker.layer.presentation()? .value(forKeyPath: keyPath) ?? maker.layer.value(forKeyPath: keyPath)
return animate(duration: duration, from: begin, to: over)
}
}
Copy the code
Add unique attributes to different core animations
extension Animation where T : CABasicAnimation {
@discardableResult
public func from(_ value:Value) -> Self {
caAnimation.fromValue = value
return self
}
@discardableResult
public func to(_ value:Value) -> Self {
caAnimation.toValue = value
return self
}
/* - `byValue' non-nil. Interpolates between the layer's current value
* of the property in the render tree and that plus `byValue'. */ @discardableResult public func by(_ value:Value) -> Self { caAnimation.byValue = value return self } }Copy the code
@available(iOSApplicationExtension 9.0, *)
extension Animation whereT : CASpringAnimation { /* The mass of the object attached to the end of the spring. Must be greater than 0. Defaults to One. */ /// Quality default 1 must be >0 @available(iOS 9.0, *) @discardableResult public func mass(_ value:CGFloat) -> Self { caAnimation.mass = valuereturnSelf} /* The spring stiffness coefficient. Must be greater than 0. * Defaults to 100. */ /// The spring stiffness coefficient Must be >0 Available (iOS 9.0, *) @discardAbleresult public func (_ value:CGFloat) -> Self {caAnimationreturnSelf} /* The damping coefficient. Must be greater than or equal to 0. * Defaults to 10. */ /// damping default 10 Must >=0 @available(iOS 9.0, *) @discardAbleresult public func damping(_ value:CGFloat) -> Self {caanimation.damping = valuereturn self
}
/* The initial velocity of the object attached to the spring. Defaults
* to zero, whichrepresents an unmoving object. Negative values * represent the object moving away from the spring attachment point, * Positive values represent the object moving towards the spring * Attachment point. */ /// A negative number indicates the initial speed @available(iOS 9.0, *) @discardableResult public func initialVelocity(_ value:CGFloat) -> Self { caAnimation.initialVelocity = valuereturn self
}
}
Copy the code
There are a few more
Finally, for some special attributes, you can point out the child attributes to do some extension additions
extension AnimationMaker wherePublic var width:AnimationMaker<Layer, CGFloat> {var width:AnimationMaker<Layer, CGFloat> {return AnimationMaker<Layer, CGFloat>(maker:maker, keyPath:"\(keyPath).width"Public var height:AnimationMaker<Layer, CGFloat> {var height:AnimationMaker<Layer, CGFloat> {return AnimationMaker<Layer, CGFloat>(maker:maker, keyPath:"\(keyPath).height")}}Copy the code
extension AnimationMaker whereValue == CATransform3D {/// public var translation:UnknowMaker<Layer, CGAffineTransform> {return UnknowMaker<Layer, CGAffineTransform>(maker:maker, keyPath:"\(keyPath).translation"Public var UnknowMaker :UnknowMaker<Layer, CGAffineTransform> {return UnknowMaker<Layer, CGAffineTransform>(maker:maker, keyPath:"\(keyPath).rotation")}}Copy the code
extension UnknowMaker wherePublic var x:AnimationMaker<Layer, CGFloat> {Value == CGAffineTransform {public var x:AnimationMaker<Layer, CGFloat> {return AnimationMaker<Layer, CGFloat>(maker:maker, keyPath:"\(keyPath).x"Public var y:AnimationMaker<Layer, CGFloat> {public var y:AnimationMaker<Layer, CGFloat> {return AnimationMaker<Layer, CGFloat>(maker:maker, keyPath:"\(keyPath).y"Public var z:AnimationMaker<Layer, CGFloat> {public var z:AnimationMaker<Layer, CGFloat> {return AnimationMaker<Layer, CGFloat>(maker:maker, keyPath:"\(keyPath).z")}}Copy the code
For the time being, I don’t have a deep understanding of the animation of more attributes of Transform, so I just wrote a few known basic attributes. In order to avoid using exceptions in the middle, I hired UnknowMaker to help fill in the blanks.
Finally, two common examples are extended
private let kShakeAnimation:String = "shakeAnimation"
private let kShockAnimation:String = "shockAnimation"Extension CALayer {public func animateShake(count:Float = 3) {extension CALayer public func animateShake(count:Float = 3) {letDistance :CGFloat = 0.08 // animate(forKey: kShakeAnimation) {
$0.transform.rotation.z. value(from: distance, to: -distance, duration: 0.1).by(0.003). Autoreverses (true).repeat(count: count).timingFunction(.easeInout)}} public func animateShock(count:Float = 2) {letDistance :CGFloat = 10 // animate(forKey: kShockAnimation) {
$0.transform.translation. X. values([0, -distance, distance, 0], duration: 0.15). Autoreverses (true).repeat(count: count).timingFunction(.easeInOut)
}
}
}
Copy the code
Finally, in order to facilitate the use and reduce the compilation time, the project is written as a library, iOS and Mac can use, because Swift 4 still does not have stable ABI library, it is suggested to drag the library into the project to use
Remember not only the Linked Frameworks but also the Embedded Binaries
Source code Github download address
If it is easy to use, please give me a Start. This article is the author’s original, if you need to reprint, please indicate the source and the original link.