Mobile Development 11 min read

A Declarative Animation Framework for iOS: Design and Implementation

The article introduces a lightweight, protocol‑oriented iOS animation framework that uses type erasure and declarative animators to replace nested callback code, providing simple factory methods, serial and parallel execution support, and clearer, more maintainable animation sequences.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
A Declarative Animation Framework for iOS: Design and Implementation

In everyday iOS development, view animations are often written with nested callbacks, for example a fade‑in followed by a scale operation:

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

When many sequential animations are required, the code quickly turns into a “callback hell” that is hard to maintain.

Several third‑party animation libraries exist, such as Spring (lightweight Swift‑based spring animations), Hero (highly customizable view transitions), and TweenKit (simple tweening). All of them still require relatively verbose code for complex sequences.

The article proposes a lightweight, declarative solution that supports both serial and parallel animation execution using a custom protocol‑oriented design.

Animator protocol defines the basic lifecycle of an animation executor:

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

Specific animation parameters are described by a hierarchy of protocols. The base protocol supplies a duration property, while CAAnimationProtocol , ProPertyAnimationProtocol , CABaseAnimationProtocol , and CAKeyFrameAnimationProtocol add properties such as repeatCount , keyPath , keyTimes , and closures that generate animation values.

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 }
}

Because the framework must store heterogeneous animators, type erasure is used. The article demonstrates this with a generic Stack class and an AnyStack wrapper that hides the concrete generic type.

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

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

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

With type erasure in place, the Animator extension provides convenient factory methods for common animations such as fadeIn and scale :

extension Animator {
    public static func fadeIn(duration: TimeInterval = 0.25,
                             curve: UIView.AnimationCurve = .linear,
                             fireAfterDelay: TimeInterval = 0.0,
                             completion: (() -> Void)? = nil) -> AnyAnimator
{
        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(values: [NSNumber],
                             keyTimes: [NSNumber],
                             repeatCount: Float = 1.0,
                             duration: TimeInterval = 0.3,
                             completion: (() -> Void)? = nil) -> AnyAnimator
{
        let animation = CAFrameKeyAnimation()
        animation.keyTimes = keyTimes
        animation.timingFunction = CAMediaTimingFunction(name: .linear)
        animation.valuesClosure = { _ in values }
        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(CAKeyFrameAnimator(animation: animation, completion: completion))
    }

    public static func creatAnimator(with propertyAnimation: PropertyAnimation,
                                   completion: (() -> Void)? = nil) -> AnyAnimator
{
        return AnyAnimator(ViewPropertyAnimator(animation: propertyAnimation, completion: completion))
    }
}

The article also explains core‑animation concepts: CAAnimation subclasses ( CABasicAnimation , CAKeyframeAnimation , CAAnimationGroup ) and the importance of the keyPath property for KVC‑based property animation.

To manage sequences and parallel groups, an AnimationToken is introduced, and UIView is extended with methods that accept an array of AnyAnimator objects. The extensions handle the recursive start‑and‑completion logic for serial execution and a counter‑based approach for parallel execution.

extension EntityWrapper where This: UIView {
    internal func performAnimations
(_ animators: [AnyAnimator
],
                                      completionHandlers: [() -> Void]) {
        guard !animators.isEmpty else {
            completionHandlers.forEach { $0() }
            return
        }
        var leftAnimations = animators
        var anyAnimator = leftAnimations.removeFirst()
        anyAnimator.start(with: this)
        anyAnimator.append {
            self.performAnimations(leftAnimations, completionHandlers: completionHandlers)
        }
    }

    internal func performAnimationsParallelism
(_ animators: [AnyAnimator
],
                                                completionHandlers: [() -> Void]) {
        guard !animators.isEmpty else {
            completionHandlers.forEach { $0() }
            return
        }
        let animationCount = animators.count
        var completionCount = 0
        let animationCompletionHandler = {
            completionCount += 1
            if completionCount == animationCount {
                completionHandlers.forEach { $0() }
            }
        }
        for animator in animators {
            animator.start(with: this)
            animator.append { animationCompletionHandler() }
        }
    }

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

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

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

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

Finally, the author concludes that the presented framework offers a lightweight way to avoid deeply nested animation callbacks, making animation code clearer and easier to evolve.

References include articles on declarative animation in Swift, Apple’s Core Animation Programming Guide, and several community resources.

animationiOSSwiftframeworkType ErasureUIViewDeclarative
NetEase Cloud Music Tech Team
Written by

NetEase Cloud Music Tech Team

Official account of NetEase Cloud Music Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.