Lightweight iOS animation framework implementation

Author of this article: Youheng

1. Background

In the daily development process, it is often necessary to animate views. If you need to animate a view: 3s to fade in, after the end, 1s to zoom in, it is easy to write such code:

UIView.animate(withDuration: 3, animations: {
    view.alpha = 1
}, completion: { _ in
    UIView.animate(withDuration: 1) {
        view.frame.size = CGSize(width: 200, height: 200)
    }
})

What if, more serial animations need to be done?

UIView.animate(withDuration: 3, animations: {
	......
}, completion: { _ in
    UIView.animate(withDuration: 3) {
				......
    }, completion: { _ in 
      	UIView.animate(withDuration: 3) {
				......
   			 }, completion: { _ in 
      			......
   			 }
    }
})

Such callback hell code is difficult to maintain and not elegant.

There are also some ready-made animation libraries in the industry, the more well-known ones are:

  • Spring: A lightweight, Swift-based animation library that provides a variety of spring animation effects. The disadvantage is that there are relatively few functions and cannot meet all animation needs.
  • Hero: A highly customizable iOS animation library that supports a variety of animation effects, such as transition animations, view transitions, etc. The disadvantage is that complex animation effects may require writing a lot of code.
  • TweenKit: A lightweight animation library based on Swift, which provides a variety of animation effects, such as gradient effects, rotation effects, etc. The advantage of TweenKit is that it is easy to use and friendly to entry-level developers, but the disadvantage is that it has relatively few functions and cannot meet all animation needs.

The above animation libraries have their own advantages and disadvantages. Generally speaking, they all have the disadvantage of relatively complicated and not elegant writing. Is there a code format that is convenient for development and maintenance?

Animation serial execution:

view.at.animate(
   .fadeIn(duration: 3.0),
   .scale(toValue: 1.2, duration: 0.5)
)

Animations are executed in parallel:

view.at.animate(parallelism:
		.fadeIn(duration: 3.0),
		.scale(toValue: 1.2, duration: 1)
)

What if multiple view combination animations are executed serially?

AT.animate (
    view1.at.animate(parallelism:
        .fadeIn(duration: 3.0),
        .scale(toValue: 1.2, duration: 1)
    )
  	view2.at.animate(parallelism:
        .fadeIn(duration: 3.0),
        .scale(toValue: 1.2, duration: 1)
		)
)

2. Implementation plan

Animator

animation executor

public protocol Animator {
    associatedtype T
    mutating func start(with view : UIView)
    func pause()
    func resume()
    func stop()
}

Encapsulate UIViewPropertyAnimator, CAKeyframeAnimator, CABasicAnimator, follow the Animator protocol, and implement different types of animation actuators.

Animation

Animation provides animation execution parameters:

Develop different Animation protocols for different Animators:

public protocol AnimationBaseProtocol {
    var duration : TimeInterval { get }
}

protocol CAAnimationProtocol : AnimationBaseProtocol {
    var repeatCount : Float { get }
    var isRemovedOnCompletion : Bool { get }
    var keyPath : String? { get }
    var animationkey : String? { get }
}

protocol ProPertyAnimationProtocol : AnimationBaseProtocol {
    var curve : UIView.AnimationCurve { get }
    var fireAfterDelay : TimeInterval { get }
    var closure : (UIView) -> Void { get }
}

protocol CABaseAnimationProtocol: CAAnimationProtocol {
    var fromValue: Any { get }
    var toValue : Any { get }
}

protocol CAKeyFrameAnimationProtocol: CAAnimationProtocol {
    var keyTimes: [NSNumber]? { get }
    var timingFunction: CAMediaTimingFunction? { get }
    var valuesClosure: ((UIView) -> [Any]?) { get }
}

It should be noted that the animation executor supports a variety of implementations, using the generic type, and the animation executor is used as the return value, which needs to be type-erased when used.

type erasure

The role of type erasure is to erase the specific information of the generic type so that it can be used at runtime:

Define a generic class:

class Stack<T> {
    var items = [T]()
    
    func push(item: T) {
        items.append(item)
    }
    
    func pop() -> T? {
        if items.isEmpty {
            return nil
        } else {
            return items.removeLast()
        }
    }
}

If used like this:

// 实例化一个 Stack<String> 对象
        let stackOfString = Stack<String>()
        stackOfString.push(item: "hello")
        stackOfString.push(item: "world")

        // 实例化一个 Stack<Int> 对象
        let stackOfInt = Stack<Int>()
        stackOfInt.push(item: 1)
        stackOfInt.push(item: 2)
        
        let stackArray: [Stack] = [stackOfString, stackOfInt]

There will be an error:

Because these are two types.

How to do the erasure?

class AnyStack {
    private let pushImpl: (_ item: Any) -> Void
    private let popImpl: () -> Any?
    
    init<T>(_ stack: Stack<T>) {
        pushImpl = { item in
            if let item = item as? T {
                stack.push(item: item)
            }
        }
        popImpl = {
            return stack.pop()
        }
    }
    
    func push(item: Any) {
        pushImpl(item)
    }
    
    func pop() -> Any? {
        return popImpl()
    }
}

In this way, the following code can be compiled and used normally:

let stackArray: [AnyStack] = [AnyStack(stackOfString), AnyStack(stackOfInt)]

Going back to the design of Animator, the same principle solves the problem of inconsistent parameter types.

Implementation
extension Animator {
    public static func fadeIn(duration: TimeInterval = 0.25, curve:UIView.AnimationCurve = .linear , fireAfterDelay: TimeInterval = 0.0, completion:(()-> Void)? = nil) -> AnyAnimator<Animation> {
        let propertyAnimation = PropertyAnimation()
        propertyAnimation.duration = duration
        propertyAnimation.curve = curve
        propertyAnimation.fireAfterDelay = fireAfterDelay
        propertyAnimation.closure = { $0.alpha = 1}
        return Self.creatAnimator(with: propertyAnimation,completion: completion)
    }
  
  public static func scale(valus: [NSNumber], keyTimes: [NSNumber], repeatCount: Float = 1.0, duration: TimeInterval = 0.3, completion:(()-> Void)? = nil) -> AnyAnimator<Animation> {
        let animation = CAFrameKeyAnimation()
        animation.keyTimes = keyTimes
        animation.timingFunction = CAMediaTimingFunction(name: .linear)
        animation.valuesClosure = {_ in valus}
        animation.repeatCount = repeatCount
        animation.isRemovedOnCompletion = true
        animation.fillMode = .removed
        animation.keyPath = "transform.scale"
        animation.animationkey = "com.moyi.animation.scale.times"
        animation.duration = duration
        return AnyAnimator.init(CAKeyFrameAnimator(animation: animation,completion: completion))
    }
  
  /// 自定义Animation
  public static func creatAnimator(with propertyAnimation : PropertyAnimation, completion:(()-> Void)? = nil) -> AnyAnimator<Animation> {
          return AnyAnimator.init(ViewPropertyAnimator(animation:propertyAnimation,completion:completion))
      }
}
    

CAAnimation 是 Core Animation 框架中负责动画效果的类,它定义了一系列动画效果相关的属性和方法。可以通过创建 CAAnimation 的子类,如 CABasicAnimation、CAKeyframeAnimation、CAAnimationGroup 等来实现不同类型的动画效果。

其中,keypath 是 CAAnimation 的一个重要概念,用于指定动画效果所作用的属性。keypath 的值通常为字符串类型,在指定属性时需要使用 KVC(键值编码)来进行访问。

更多关于 CAAnimation 的内容可以参考引用中相关链接,不是本文重点不再展开。

AnimationToken

AnimationToken 是视图和动画执行器的封装,用于视图的动画处理。

然后对 UIView 添加串行、并行的扩展方法:

extension EntityWrapper where This: UIView {
    internal func performAnimations<T>(_ animators: [AnyAnimator<T>] , completionHandlers: [(() -> Void)]) {
        guard !animators.isEmpty else {
            completionHandlers.forEach({ handler in
                handler()
            })
            return
        }
        
        var leftAnimations = animators
        var anyAnimator = leftAnimations.removeFirst()
        
        anyAnimator.start(with: this)
        anyAnimator.append {
            self.performAnimations(leftAnimations, completionHandlers: completionHandlers)
        }
    }
    
    internal func performAnimationsParallelism<T>(_ animators: [AnyAnimator<T>], completionHandlers: [(() -> Void)]) {
        guard !animators.isEmpty else {
            completionHandlers.forEach({ handler in
                handler()
            })
            return
        }
        
        let animationCount = animators.count
        var completionCount = 0
        
        let animationCompletionHandler = {
            completionCount += 1
            if completionCount == animationCount {
                completionHandlers.forEach({ handler in
                    handler()
                })
            }
        }

        for var animator in animators {
            animator.start(with: this)
            animator.append {
                animationCompletionHandler()
            }
        }
    }
}

completionHandlers 是动画任务的结束的回调逻辑,类似 UIView 类方法 animate 的 completion 回调,这样就有了动画结束的回调能力。

给 UIView 添加扩展,实现 view.at.animate () 方法:

extension EntityWrapper where This: UIView {
    
    @discardableResult private func animate<T>(_ animators: [AnyAnimator<T>]) -> AnimationToken<T> {
        return AnimationToken(
            view: this,
            animators: animators,
            mode: .inSequence
        )
    }

    @discardableResult public func animate<T>(_ animators: AnyAnimator<T>...) -> AnimationToken<T> {
        return animate(animators)
    }

    @discardableResult private func animate<T>(parallelism animators: [AnyAnimator<T>]) -> AnimationToken<T> {
        return AnimationToken(
            view: this,
            animators: animators,
            mode: .parallelism
        )
    }

    @discardableResult public func animate<T>(parallelism animators: AnyAnimator<T>...) -> AnimationToken<T> {
        return animate(parallelism: animators)
    }
}

AT.animate () 对 AnimationToken 进行串行管理,不再赘述。

三、总结

本文只是对动画回调嵌套问题的轻量化解决方案,让组合动画的代码结构更加清晰,方便开发和后续迭代修改。实现方案还有许多可以改进的地方,欢迎参考指正。

四、参考资料

  1. 图源:unsplash.com/photos/PDxp…
  2. [Declarative animation]www.swiftbysundell.com/articles/bu…
  3. [CAAnimation] Apple Inc. Core Animation Programming Guide. [About Core Animation](About Core Animation)
  4. [CAAnimation] 王巍. iOS 动画高级技巧 [M]. 北京:人民邮电出版社,2015.
  5. [CAAnimation]developer.apple.com/documentati…
  6. [CAAnimation]github.com/pro648/tips…
  7. [UIViewPropertyAnimator]developer.apple.com/documentati…
  8. [使用 UIViewPropertyAnimator 做动画]swift.gg/2017/04/20/…

本文发布自网易云音乐技术团队,文章未经授权禁止任何形式的转载。我们常年招收各类技术岗位,如果你准备换工作,又恰好喜欢云音乐,那就加入我们 grp.music-fe (at) corp.netease.com!

Guess you like

Origin juejin.im/post/7252586606091419708