iosswiftanimationswiftuigesture-state

Animated Object with @GestureState shows console error "Invalid sample AnimatablePair"


I'm trying to get a "true" value when the user uses gestures to move the 300x300 rectangle and a "false" value when they finish interacting with gestures.

In this case it seemed logical to use @GestureState

As you can see in the .updating() call inside DragGesture I used @GestureState private var isDragging: Bool = false

@GestureState should change the opacity of the 100x100 red rectangle

 public struct ContentView: View {
    @State private var offset: CGSize = .zero
    @State private var lastOffset: CGSize = .zero
    @GestureState private var isDragging: Bool = false
    
    public init() { }

    public var body: some View {
       
        VStack(spacing: 40) {
           
            Rectangle().fill(.red)
                .frame(width: 100, height: 100)
                .opacity(isDragging ? 0 : 1)

            Rectangle()
                .frame(width: 300, height: 300)
                .offset(offset)
                .gesture(
                    DragGesture()
                        .updating($isDragging, body: { _, out, _ in
                            out = true
                        })
                        .onChanged({ value in
                            let currentOffset: CGSize = .init(width: lastOffset.width + value.translation.width, height: lastOffset.height + value.translation.height)
                            offset = currentOffset
                        })
                        .onEnded({ _ in
                            lastOffset = offset
                        })
                )
        }
        .animation(.default, value: isDragging)
    }
}

everything seems to work fine but when I add .animation(.default, value: isDragging) the console gives me this error while I use the dragGesture and the red rectangle opacity animation is running

Invalid sample AnimatablePair<AnimatablePair<CGFloat, CGFloat>, AnimatablePair<CGFloat, CGFloat>>(first: SwiftUI.AnimatablePair<CoreGraphics.CGFloat, CoreGraphics.CGFloat>(first: -23.666666666666664, second: -19.0), second: SwiftUI.AnimatablePair<CoreGraphics.CGFloat, CoreGraphics.CGFloat>(first: 0.0, second: 0.0)) with time Time(seconds: 0.0) > last time Time(seconds: 0.016666666720993817)

and I don't understand why... if I don't use animations everything works correctly otherwise it tells me this problem...

Where am I wrong? isn't this the right way to animate objects based on the use of gestures? are there other ways? is this the wrong way?


Solution

  • I found that it helps to add animation to other state changes affected by the drag gesture. In your example, this means animating changes to offset too.

    Here are two possible ways to solve:

    1. Add .animation modifiers for other variables

    For the example here, add a modifier for offset:

    VStack(spacing: 40) {
        //...
    }
    .animation(.default, value: isDragging)
    .animation(.easeInOut(duration: 0.1), value: offset) // 👈 here
    

    If you don't really want any additional animation then a duration of 0 seems to work. However, supplying nil as the animation type does not work and still reports errors in the console.

    2. Perform changes using withAnimation

    An alternative way to solve is to use withAnimation for updates performed in gesture callbacks, where these changes are not already covered by .animation modifiers:

    .onChanged({ value in
        let currentOffset: CGSize = .init(width: lastOffset.width + value.translation.width, height: lastOffset.height + value.translation.height)
        withAnimation(.easeInOut(duration: 0.1)) { // 👈 here
            offset = currentOffset
        }
    })
    

    As in point 1, a duration of 0 can be used, but an animation type of nil does not work and still reports errors in the console. Performing the changes using a Transaction with disablesAnimations = true also reports errors.


    You were asking:

    isn't this the right way to animate objects based on the use of gestures? are there other ways? is this the wrong way?

    I would suggest, it is more conventional to use a GestureState for a value that is changing during the gesture, rather than for a boolean flag that signals that the gesture is happening. This avoids the need for an additional .onChanged modifier.

    Here is how your example could be re-factored by using a GestureState for the drag offset. Things to note:

    public struct ContentView: View {
        @State private var offset: CGSize = .zero
        @GestureState private var dragOffset = CGSize.zero
    
        private var isDragging: Bool {
            dragOffset != .zero
        }
    
        private func combinedOffset(adding: CGSize) -> CGSize {
            CGSize(
                width: offset.width + adding.width,
                height: offset.height + adding.height
            )
        }
    
        public var body: some View {
            VStack(spacing: 40) {
    
                Rectangle()
                    .fill(.red)
                    .frame(width: 100, height: 100)
                    .opacity(isDragging ? 0 : 1)
                    .animation(.default, value: isDragging)
    
                Rectangle()
                    .frame(width: 300, height: 300)
                    .offset(combinedOffset(adding: dragOffset))
                    .animation(.easeInOut(duration: 0.1), value: dragOffset)
                    .gesture(
                        DragGesture()
                            .updating($dragOffset) { val, out, _ in
                                out = val.translation
                            }
                            .onEnded { val in
                                offset = combinedOffset(adding: val.translation)
                            }
                    )
            }
        }
    }