6

SwiftUI offers .animation() on bindings that will animate changes in the view. But if an @Published property from an @ObserveredObject changes 'autonomously' (e.g., from a timer), while the view will update in response to the change, there is no obvious way to get the view to animate the change.

In the example below, when isOn is changed from the Toggle, it animates, but when changed from the Timer it does not. Interestingly, if I use a ternary conditional here rather than if/else even the toggle will not trigger animation.

struct ContentView: View {
    @ObservedObject var model: Model
    var body: some View {
        VStack {
            if model.isOn {
                MyImage(color: .blue)
            } else {
                MyImage(color: .clear)
            }
            Spacer()
            Toggle("switch", isOn: $model.isOn.animation(.easeIn(duration: 0.5)))
            Spacer()
        }
    }
}

struct MyImage: View {
    var color: Color
    var body: some View {
        Image(systemName: "pencil.circle.fill")
            .resizable()
            .frame(width: 100, height: 100)
            .foregroundColor(color)
    }
}

class Model: ObservableObject {
    @Published var isOn: Bool = false
    var timer = Timer()
    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { [unowned self] _ in
            isOn.toggle()
        })
    }
}

How can I trigger animations when the value changes are not coming from a binding?

c_booth
  • 2,185
  • 1
  • 13
  • 22

2 Answers2

11

The easiest option is to add a withAnimation block inside your timer closure:

withAnimation(.easeIn(duration: 0.5)) {
  isOn.toggle()
}

If you don't have the ability to change the @ObservableObject closure, you could add a local variable to mirror the changes:

struct ContentView: View {
    @ObservedObject var model: Model
    @State var localIsOn = false
    var body: some View {
        VStack {
            if localIsOn {
                MyImage(color: .blue)
            } else {
                MyImage(color: .clear)
            }
            Spacer()
            Toggle("switch", isOn: $model.isOn.animation(.easeIn(duration: 0.5)))
            Spacer()
        }.onChange(of: model.isOn) { (on) in
            withAnimation {
                localIsOn = on
            }
        }
    }
}

You could also do a similar trick with a mirrored variable inside your ObservableObject:


struct ContentView: View {
    @ObservedObject var model: Model
    var body: some View {
        VStack {
            if model.animatedOn {
                MyImage(color: .blue)
            } else {
                MyImage(color: .clear)
            }
            Spacer()
            Toggle("switch", isOn: $model.isOn.animation(.easeIn(duration: 0.5)))
            Spacer()
        }
    }
}


class Model: ObservableObject {
    @Published var isOn: Bool = false
    @Published var animatedOn : Bool = false
    
    var cancellable : AnyCancellable?
    
    var timer = Timer()
    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { [unowned self] _ in
                isOn.toggle()
        })
        cancellable = $isOn.sink(receiveValue: { (on) in
            withAnimation {
                self.animatedOn = on
            }
        })
    }
}
jnpdx
  • 45,847
  • 6
  • 64
  • 94
1

You can use an implicit animation for that, i.e. .animation(_:value:), e.g.

struct ContentView: View {
    @ObservedObject var model: Model

    var body: some View {
        VStack {
            Group {
                if model.isOn {
                    MyImage(color: .blue)
                } else {
                    MyImage(color: .clear)
                }
            }
            .animation(Animation.default, value: model.isOn)

        }
    }
}

withAnimation is called explicit.

malhal
  • 26,330
  • 7
  • 115
  • 133
  • I've the same structure, except instead of ObservedObject, I've Binding value that controls the group view. I don't see any animation. Am I missing something here? – kelalaka Mar 01 '23 at 11:48
  • 1
    With a binding it is `$property.animation()` – malhal Mar 01 '23 at 15:21
  • Thanks for the reply. Where to put this line assuming that model is the binding? Currently solved this `withAnimation` where the binding is changed and transitions on the views. – kelalaka Mar 01 '23 at 15:34