13

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.

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 }
            }
        
    }
}

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 tutorials about the Animatable protocol I can find online all use examples of 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".

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 }
            }
    }
}

Thanks for your kind reading, and maybe oncoming answers!

Gwozi
  • 133
  • 5
  • Second example works fine. Xcode 13.2 / iOS 15.2. Simulator/Device. – Asperi Jan 22 '22 at 10:15
  • It seems that the second example works in iOS 15 but not in iOS 14 – Gwozi Jan 30 '22 at 07:32
  • Does this answer your question? [How to replace deprecated .animation() in SwiftUI?](https://stackoverflow.com/questions/69443588/how-to-replace-deprecated-animation-in-swiftui) – burnsi Jan 31 '22 at 11:07
  • 1
    On Xcode 13.3.1/iOS 15.4 simulator, the second example *doesn't* work. Not the the intended purpose: the setter on animatableData is called ahead of time to generate *all* the values before animating, so you think you've finished the animation before it's even started. So it's not useful for detecting the end of the animation. to see what I mean, make the animation be .linear(duration = 10) -- you will see the values before the 10 seconds are up. – occulus Jun 02 '22 at 17:24

2 Answers2

3

Please use struct OffsetAnimationView: View, Animatable instead of struct OffsetAnimation: AnimatableModifier directly in iOS 15 and above.

In iOS 15 and above, I only use Animatable to implement animations by inheriting View and Animatable, without using ViewModifier or AnimatableModifier. However, in iOS versions below 15, I have to use AnimatableModifier and then use Color.clear.modifier(...) to apply it. For each animation in the application, I wrote it twice using both methods.

For Example:

struct FooAnimatableModifier: AnimatableModifier {
    
    let oldValue: Int
    let newValue: Int
    
    var pct: CGFloat
    
    var animatableData: CGFloat {
        get { pct }
        set { pct = newValue }
    }
    
    var currentValue: CGFloat {
        CGFloat(oldValue) + (CGFloat(newValue) - CGFloat(oldValue)) * pct
    }
    
    func body(content: Content) -> some View {
        FooStaticView(value: currentValue)
    }
    
}

struct FooAnimatableView: View, Animatable {
    
    let oldValue: Int
    let newValue: Int
   
    var pct: CGFloat
    
    var animatableData: CGFloat {
        get { pct }
        set { pct = newValue }
    }
    
    var currentValue: CGFloat {
        CGFloat(oldValue) + (CGFloat(newValue) - CGFloat(oldValue)) * pct
    }
    
    var body: some View {
        FooStaticView(value: currentValue)
    }
    
    
}

struct FooStaticView: View {
    
    var value: CGFloat
    
    var body: some View {
        Text("\(value)")
    }
    
}

struct FooView: View {
    
    @State var isAnimated = false
    
    var body: some View {
        Group {
            if #available(iOS 15, *) {
                FooAnimatableView(oldValue: 20, newValue: 80, pct: isAnimated ? 1 : 0)
            } else {
                Color.clear.modifier(
                    FooAnimatableModifier(oldValue: 20, newValue: 80, pct: isAnimated ? 1 : 0)
                )
            }
        }
        .animation(.linear(duration: 0.5), value: isAnimated)
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                isAnimated = true
            }
        }
    }
    
}

Avoid using it inside a container(such as a VStack) below iOS 13.2.

myself
  • 79
  • 4
  • 2
    It doesn't work for me too. if I use AnimatableModifier direct then the animation works. If I conform using Animatable, ViewModifier then animation doesn't work. – sliwinski.lukas Mar 07 '22 at 14:55
  • Yes, I got the same problem. If I replace the ` : AnimatableModifier` directly with `:Animatable, ViewModifier`, then animation just doesn't work any more. Have you ever solved this problem? @sliwinski.lukas – Neal.Marlin Jul 19 '22 at 07:14
  • Nope, it seems like this is an iOS problem that cannot be easily fixed. I changed my implementation to use transforms instead with great effect. – sliwinski.lukas Jul 29 '22 at 07:08
  • Different implementation methods are required for iOS versions below 15 and for iOS 15 and above. `AnimatableModifier` should be used for versions below 15, while `Animatable` can be used directly for versions 15 and above. @Neal.Marlin @sliwinski.lukas – myself May 18 '23 at 07:02
  • 1
    @myself Would you please show me the different implementation methods , when using `Animatable `, for version below and above version 15? I think that may be my problem. – Neal.Marlin May 27 '23 at 15:01
2

Try this (working, Xcode 13.4.1, iOS 15.5, inspired by https://www.avanderlee.com/swiftui/withanimation-completion-callback):

struct OffsetAnimation: ViewModifier, Animatable {
    typealias T = CGFloat
    var animatableData: T {
        didSet {
            animatableDataSetAction()
        }
    }
    
    private func animatableDataSetAction() {
        guard animatableData == targetValue else { return }
        DispatchQueue.main.async {
            self.onCompletion()
        }
    }
    
    var targetValue: T
    init(value: T, onCompletion: @escaping()->()){
        self.targetValue = value
        self.animatableData = value
        self.onCompletion = onCompletion
    }
    var onCompletion: () -> ()
    func body(content: Content) -> some View {
        return content.offset(x: 0, y: targetValue)
    }
}

struct DemoView: View {
    @State var offsetY: CGFloat = .zero
    @State private var myText = "Animating"
    var body: some View {
        VStack {
            
            Text(myText).padding()
            Rectangle().frame(width: 100, height: 100, alignment: .center)
                .modifier(
                    OffsetAnimation(value: offsetY,
                                    onCompletion: {myText = "Translation finished"}))
                .onAppear{
                    withAnimation(.easeIn(duration: 3.0)){ 
                        offsetY = 100 
                   }
                }                
        }  
    }
}
Torrontés
  • 145
  • 7