Animation Effect

Hey everyone,

Lately, I've been working a lot with animations and I've been copying code from the Animation case study where an animation consists of multiple Effects run successively that mutate the state for each step of the animation. It works great but when you end up having lots of animation, it's kinda cumbersome to copy paste this logic for each animation. So I was thinking of an .animation Effect that we could use instead that simply wraps the logic into another Effect.

Basically, it could look like this :

import Combine

public protocol AnimationDuration {
    var duration: Double { get }
}

extension Effect where Failure == Never {
    public static func animation<State, S>(
        animationStates: [State],
        action: @escaping (State) -> Output,
        scheduler: S
    ) -> Effect where State: AnimationDuration, S: Scheduler {
        return Effect.concatenate(
            animationStates
            .enumerated()
            .map { index, animationState in
                index == 0
                ? Effect(value: action(animationState))
                : Effect(value: action(animationState))
                .delay(for: .seconds(animationStates[index - 1].duration), scheduler: scheduler)
                .eraseToEffect()
            }
        )
    }
}

And the usage (taken from the case study):

struct AnimationsState: Equatable {
  struct Animation: AnimationDuration, Equatable {
    let duration: Double
    let color: Color
  }

  var circleColor = Color.white
}

enum AnimationsAction: Equatable {
  case rainbowButtonTapped
  case setColor(AnimationsState.Animation)
}

struct AnimationsEnvironment {
  var mainQueue: AnySchedulerOf<DispatchQueue>
}

let animationsReducer = Reducer<AnimationsState, AnimationsAction, AnimationsEnvironment> {
  state, action, environment in

  switch action {
  case .rainbowButtonTapped:
    // You need to declare each step of your animation for the animation Effect
    // with a duration and a property to animate at the very least.
    // You can also have different animations (linear, ease in, ease out etc.) if you want.
    let animationStates = [Color.red, .blue, .green, .orange, .pink, .purple, .yellow, .white]
        .map { AnimationsState.Animation(duration: 1, color: $0) }
    return .animation(
      animationStates: animationStates,
      action: { .setColor($0) },
      scheduler: environment.mainQueue
    )

  case let .setColor(animation):
    state.circleColor = animation.color
    return .none
}

Let me know what you think and if it's worth creating a PR for this.

Hey @Dabou, that's very cool!

I think there's a few small things that could be done to streamline a few things. For example, I don't think the protocol is really necessary. What about just having the static func take all the information upfront (and maybe rename it):

extension Effect where Failure == Never {
  public static func keyFrames<S>(
    values: [(output: Output, duration: S.SchedulerTimeType.Stride)],
    scheduler: S
  ) -> Effect where S: Scheduler {
    .concatenate(
      values
        .enumerated()
        .map { index, animationState in
          index == 0
            ? Effect(value: animationState.output)
            : Just(animationState.output)
                .delay(for: values[index - 1].duration, scheduler: scheduler)
                .eraseToEffect()
        }
    )
  }
}

And then at the call site it could look like this:

  case .rainbowButtonTapped:
    return .keyFrames(
      values: [.red, .blue, .green, .orange, .pink, .purple, .yellow, .white]
        .map { (output: .setColor($0), duration: 1) },
      scheduler: environment.mainQueue
    )

I think the key take away from this is that it's definitely useful to extend Effect with additional functionality. I'm not sure we want to bring this into the core library, but I think it would definitely be nice to have it in the case study. Want to give that a shot?

1 Like

I'm not sure we want to bring this into the core library, but I think it would definitely be nice to have it in the case study. Want to give that a shot?

For sure ! Should I just add the extension to the case study and update the call or is there a more appropriate location for the extension ?

Here's the PR : Add keyFrames Effect in animations case study by boudavid · Pull Request #231 · pointfreeco/swift-composable-architecture · GitHub

3 Likes