SwiftUI Animation from @Published property changing from outside the View
Asked Answered
E

2

10

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?

Everara answered 22/4, 2021 at 20:3 Comment(0)
C
16

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
            }
        })
    }
}
Convoy answered 22/4, 2021 at 20:26 Comment(0)
F
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.

Finical answered 15/1, 2023 at 1:29 Comment(3)
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?Scapolite
With a binding it is $property.animation()Finical
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.Scapolite

© 2022 - 2025 — McMap. All rights reserved.