You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过拖拽手势平滑中断SwiftUI动画?

实现拖拽手势中断动画时的平滑无跳变交互

Apple在2023年WWDC中大幅简化了拖拽手势后视图的平滑动画实现,可自动将拖拽手势的最终速度作为动画初始速度。以下是基础示例:

struct DraggableCircle: View {
    @State private var offset: CGSize = .zero
    
    var body: some View {
        let gesture = DragGesture(minimumDistance: 0)
            .onChanged { gesture in
                offset = gesture.translation
            }
            .onEnded { _ in
                withAnimation(.bouncy(duration: 2)) {
                    offset = .zero
                }
            }
        
        Circle()
            .frame(width: 200, height: 200)
            .offset(offset)
            .gesture(gesture)
    }
}

动画示例

但这段代码存在缺陷——当动画尚未结束时触发新的拖拽手势,无法平滑中断现有动画,圆形会瞬间完成动画并从手指位置跳开:

跳变问题示例

问题根源在于:手势结束时我们直接将offset设回.zero,完全没有考虑动画中断时圆形的实际屏幕位置。

有一种替代方案是直接用手势位置定位圆形,确保它始终跟随手指,但这种方法会强制圆形中心对齐手指位置,完全忽略了手势起始于圆形的哪个区域,也没解决动画中断时获取实时位置的核心问题:

struct DraggableCircle_v2: View {
    @State private var position: CGPoint?
    
    var body: some View {
        let gesture = DragGesture(minimumDistance: 0)
            .onChanged { gesture in
                position = gesture.location
            }
            .onEnded { _ in
                withAnimation(.bouncy(duration: 2)) {
                    position = nil
                }
            }
        
        GeometryReader { geo in
            let center = CGPoint(
                x: geo.size.width / 2,
                y: geo.size.height / 2
            )
            
            Circle()
                .frame(width: 200, height: 200)
                .position(position ?? center)
                .gesture(gesture)
        }
    }
}

方案不足示例

正确实现:平滑中断动画+无跳变拖拽

要实现目标效果,核心是在拖拽触发时获取视图的动画实时位置,以此作为新拖拽的起始基准,同时确保动画可被中断。以下是完整实现代码:

struct DraggableCircle_Smooth: View {
    // 记录视图的目标偏移量(动画过程中会逐渐变化)
    @State private var targetOffset: CGSize = .zero
    // 拖拽过程中的临时偏移量,手势结束后自动重置
    @GestureState private var dragOffset: CGSize = .zero
    
    var body: some View {
        let gesture = DragGesture(minimumDistance: 0)
            .updating($dragOffset) { value, state, _ in
                // 基于当前动画的实时位置(targetOffset)加上新拖拽的平移量
                state = targetOffset + value.translation
            }
            .onEnded { value in
                // 更新目标偏移为拖拽结束时的位置
                targetOffset = targetOffset + value.translation
                // 启动回到原点的可中断动画
                withAnimation(.bouncy(duration: 2).interruptible()) {
                    targetOffset = .zero
                }
            }
        
        Circle()
            .frame(width: 200, height: 200)
            // 拖拽时用实时偏移,动画时用目标偏移
            .offset(dragOffset != .zero ? dragOffset : targetOffset)
            .gesture(gesture)
    }
}

方案原理

  1. @GestureState的作用:存储拖拽过程中的临时偏移量,手势结束后会自动重置,避免干扰后续动画状态
  2. 实时位置衔接:拖拽开始时,updating闭包基于当前targetOffset(即动画中断时的实时位置)计算新的拖拽偏移,确保圆形从当前位置平滑跟随手指,不会跳变
  3. 可中断动画:使用.interruptible()修饰动画,允许新的拖拽手势直接中断正在进行的动画,无需等待动画完成
  4. 状态分离:targetOffset负责记录动画的目标位置,dragOffset负责拖拽过程中的实时位置,两者配合实现无缝衔接

预期效果示例

内容的提问来源于Stack Exchange,提问作者Tim Vermeulen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 13:27:03