So... AnimatableModifier is now deprecated, but how to "use Animatable directly"?

I am aiming to achieve a callback when an animation of offset change is finished. So, I found a workaround online which uses the AnimatableModifier to check when the animatableData equals the target value.

But it turns out AnimatableModifier is now deprecated. And I cannot find an alternative to it.

I am aware that GeometryEffect will work for this case of offset change, where you could use ProjectionTransform to do the trick. But I am more concerned about the official recommendation to "use Animatable directly".

Seriously, the only tutorial about the Animatable protocol I can find online is examples of using the Shape struct which implicitly implements the Animatable protocol.

And the following code I improvised with the Animatable protocol doesn't even do the "animating".

Thanks for your kind reading, and maybe oncoming answers!

Accepted Reply

That would really help to post the code also as text.

Replacing by

    var body: some View {
        RectView(value: offsetY)
            .onAppear {
                withAnimation(.easeInOut) { offsetY = 100 }
            }
    }

gives an animation effect.

You can also adjust speed:

                withAnimation(.easeInOut.speed(0.1)) { offsetY = 100 }

I also removed .animation(nil)

    var body: some View {
        Rectangle()
            .frame(width: 100, height: 100, alignment: .center)
            .offset(y: value)
    }
  • Sorry, I think I made the mistake of accepting your solution. After a second thought and test, it seems that in your solution, the animation works only because you removed the .animation(nil) and the default modifier .offset() fires the animation under the hood. One testiment is that print("animating \(value)") is not executed.

  • What was the purpose of animation(nil) ?

  • to suppress the .offset()'s default animation behavier and make sure the interpolation of the animatable values happens in the RectView, i.e. the set { value = newValue; print("animating \(value)") } will be called and then check for completion of the animation.

Add a Comment

Replies

That would really help to post the code also as text.

Replacing by

    var body: some View {
        RectView(value: offsetY)
            .onAppear {
                withAnimation(.easeInOut) { offsetY = 100 }
            }
    }

gives an animation effect.

You can also adjust speed:

                withAnimation(.easeInOut.speed(0.1)) { offsetY = 100 }

I also removed .animation(nil)

    var body: some View {
        Rectangle()
            .frame(width: 100, height: 100, alignment: .center)
            .offset(y: value)
    }
  • Sorry, I think I made the mistake of accepting your solution. After a second thought and test, it seems that in your solution, the animation works only because you removed the .animation(nil) and the default modifier .offset() fires the animation under the hood. One testiment is that print("animating \(value)") is not executed.

  • What was the purpose of animation(nil) ?

  • to suppress the .offset()'s default animation behavier and make sure the interpolation of the animatable values happens in the RectView, i.e. the set { value = newValue; print("animating \(value)") } will be called and then check for completion of the animation.

Add a Comment

I can't seem to update my post now. But the two code snippets are pasted below.

struct OffsetAnimation: AnimatableModifier{
    typealias T = CGFloat
    var animatableData: T{
        get { value }
        set {
            value = newValue
            print("animating \(value)")
            if watchForCompletion && value == targetValue {
                DispatchQueue.main.async { [self] in onCompletion() }
            }
        }
    }
    var watchForCompletion: Bool
    var value: T
    var targetValue: T
    init(value: T, watchForCompletion: Bool, onCompletion: @escaping()->()){
        self.targetValue = value
        self.value = value
        self.watchForCompletion = watchForCompletion
        self.onCompletion = onCompletion
    }
    var onCompletion: () -> ()
    func body(content: Content) -> some View {
        return content.offset(x: 0, y: value).animation(nil)
    }
}

struct DemoView: View {
    @State var offsetY: CGFloat = .zero
    var body: some View {
        Rectangle().frame(width: 100, height: 100, alignment: .center)
            .modifier(
                OffsetAnimation(value: offsetY,
                                watchForCompletion: true,
                                onCompletion: {print("translation complete")}))
            .onAppear{
                withAnimation{ offsetY = 100 }
            }
        
    }
}
struct RectView: View, Animatable{
    typealias T = CGFloat
    var animatableData: T{
        get { value }
        set {
            value = newValue
            print("animating \(value)")
        }
    }
    var value: T
    var body: some View{
        Rectangle().frame(width: 100, height: 100, alignment: .center)
            .offset(y:value).animation(nil)
    }
}

struct DemoView: View{
    @State var offsetY: CGFloat = .zero
    var body: some View {
        RectView(value: offsetY)
            .onAppear{
                withAnimation{ offsetY = 100 }
            }
    }
}

Conform your view modifier to ViewModifier and Animatable explicitly.

  • @Peter_Schorn I tried but on iOS 15 if I conform explicitly to ViewModifer and Animatable an animation doesn't work at all. If I conform to AnimatableModifier then it somehow works. On the other hand, if I conform to AnimatableModifier then on iOS 13.0 the app crashes (cannot find probably the protocol in SwiftUI framework - probably a bug).

Add a Comment