Performing effects with animation

The animations use case mentions a few ways to work with SwiftUI animations: calling send inside an withAnimations closure or use a binding with animations.

I was wondering how we are supposed to approach animations that are triggered by actions emitted from effects. In that case the send is invoked in the effect's sink inside Store. I don't see a way to hook into that.

Is there a way to achieve animations with effects currently, or should we discuss having "animatable" effects (e.g. similarly to how we can have cancellable effects)?

As long as the animation can be driven off of state I think you can just use the .animation() view modifiers. We just updated our animation case study to have the circle change colors on a timer, which is driven off of effects (see here).

Do you have an example of animation that doesn't fall into this simple use case? Could you share a lil demo app to show what you are dealing with?

The .animation() modifier would work indeed but in a non-trivial view state changes could affect many aspects of the view and you might want to determine the type of animation depending on why you make that change. I think that's why withAnimation exists and why it's useful to be able to call send wrapped in withAnimation.

Building on your updated case study, imagine I would like to be able to specify a different animation for each color change. A less contrived example would be a submit button that changes color depending on its state (inactive, active, loading) and you want the animation of the background color/scale/etc of the button to happen differently depending on which states it is transitioning between.

I don't have a concrete demo ready as I wanted to start the discussion before implementing something that would be blocked by this limitation.

Yeah that's true... it would be best to have some way to wrap certain effects' sends in a withAnimation :thinking:

We'll think about it, and let us know if you come up with anything.

I don't know if I have a different example or I'm doing something wrong. In a macOS app i have a list driven off some state and two buttons: one to add a new item and, after a delay, random sort the list, and the other to directly sort the list.

let reducer = Reducer { state, action, environment in
  switch action {
  case .add:
    state.listOfItems.append(Item())
    return Effect(value: .sort).delay(...).eraseToEffect()
  case .sort:
    state.listOfItems.randomSortInPlace()
    return .none
  }
}

struct Header: View {
  let store: Store<SomeState, SomeAction>

  var body: some View {
    WithViewStore(store) { viewStore in
      HStack {
        Button("Add") {
          withAnimation { viewStore.send(.add) }
        }

        Spacer()

        Button("Sort") {
          withAnimation { viewStore.send(.sort) }
        }
      }
    }
  }
}

struct Container: View {
  let store: Store<SomeState, SomeAction>

  var body: some View {
    List {
      Section(header: Header(store: store)) {
        ForEachStore(
          store.scope(state: \.listOfItems, action: SomeAction.itemAction),
          content: ItemView.init)
      }
    }
  }
}

If I use the "Sort" button I can see the animation, but if the .sort action is that sent from the Effect there is no animation, even if i use withAnimation inside the reducer.

Ah interesting, thanks for the example.

I'm not entirely sure why the list isn't sorting in that case. We have a very similar situation in the Todo demo app where we sort the list after a small delay (done with an Effect):

Can you see anything different from how we are doing it in that app from how you are doing it?

Yeah, in fact I followed your Todo example :confused: the only difference seems to be the target os and that I have a delay instead of a debounce... the scheduler is DispatchQueue.main... there may be something different going on under the hood between Mac and iPhone?

I can confirm that on iOS it works normally... I created a new target and shared the existing code (no rewriting), the animation was there both on manual sort and on Effect sort :pensive:

Ah good to know! Maybe a feedback could be filed for macOS then.

Here it is:
https://github.com/pointfreeco/swift-composable-architecture/issues/207

Maybe there is a video or a blog post I missed, but I can't find how

struct TodoCompletionId: Hashable {}

is supposed to work (I mean the pattern itself, not the case, and it's more related to dependencies like AudioPlayerClient's ones in VoiceMemos example). Any empty Hashable struct will have the same hashValue which doesn't depend on the declaration context or type name :confused:, so we'll have the same ID in every dependent reducer for any dependency, in any module...

The hash values may be equal but the values themselves are not equal:

struct Id1: Hashable {}
struct Id2: Hashable {}

AnyHashable(Id1()) == AnyHashable(Id1()) // true
AnyHashable(Id2()) == AnyHashable(Id2()) // true
AnyHashable(Id1()) == AnyHashable(Id2()) // false

So an empty struct can be used to uniquely identify an effect. Does that help?

2 Likes

Yes, it's clear now, thank you for the explanation :partying_face:
Also worth mentioning that Dictionary<AnyHashable: Value> doesn't identify contents by its key's hashValue as I thought.

I found this post as I was looking for the same thing. I wanted to share the approach I'm taking when state change is triggered from within the reducer itself. Basically I've added an animation property to my state and then I use the animation modifier with it. This gives me enough flexibility.