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

如何实现ScrollView中任务卡片固定/取消固定时的位移动画

实现ScrollView中任务卡片跨分组的位移动画方案

问题场景

我有一个SwiftUI的ScrollView,顶部是固定(pinned)任务项,底部是非固定(unpinned)任务项,布局如下:

______________
|    Item 1    |
|    Item 2    |
|    Item 3    |
|--------------|
|    Item 4    |
|    Item 5    |
|    Item 6    |
|    Item 7    |
|    Item 8    |
|______________|

通过两个ForEach分别渲染两个集合:

var pinnedItems = ["Item 1", "Item 2", "Item 3"]
var unpinnedItems = ["Item 4", "Item 5", "Item 6", "Item 7", "Item 8"]

现有实现代码:

ScrollView {
    ForEach(pinnedTasks) { task in
        NavigationLink(value: NavigationPath.task(task)) {
            TaskCardView(task: task, onPinning: {
                withAnimation {
                    togglePinnedTask(task: task)
                }
            })
            .padding(.bottom)
        }.buttonStyle(.plain)
    }

    Divider()
        .frame(minHeight: 3)
        .background(.gray)
        .padding(.bottom)

    ForEach(unPinnedTasks) { task in
        NavigationLink(value: NavigationPath.task(task)) {
            TaskCardView(task: task, onPinning: {
                withAnimation {
                    togglePinnedTask(task: task)
                }
            })
            .padding(.bottom)
        }.buttonStyle(.plain)
    }
}

togglePinnedTask()函数负责将任务从一个集合移到另一个集合,当前默认只有淡入淡出动画,想要实现任务卡片“飞”到新位置的位移动画。

实现方案

核心是利用SwiftUI的matchedGeometryEffect,它能让同一个视图在不同布局位置间产生平滑的位移动画,具体步骤如下:

1. 定义全局命名空间

在视图结构体中声明一个命名空间,用于标识需要匹配的几何效果:

@Namespace private var animationNamespace

2. 给TaskCardView添加matchedGeometryEffect

修改两个ForEach中的TaskCardView,为其添加matchedGeometryEffect,用任务的唯一标识作为id:

// 固定分组中的卡片
TaskCardView(task: task, onPinning: { ... })
    .padding(.bottom)
    .matchedGeometryEffect(id: task.id, in: animationNamespace)

// 非固定分组中的卡片
TaskCardView(task: task, onPinning: { ... })
    .padding(.bottom)
    .matchedGeometryEffect(id: task.id, in: animationNamespace)

注意:确保每个任务遵循Identifiable协议,拥有唯一的id,这样系统才能正确匹配同一个任务在不同分组中的视图。

3. 优化动画触发逻辑

保持withAnimation包裹togglePinnedTask的调用,可指定弹簧动画让移动效果更自然:

withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
    togglePinnedTask(task: task)
}

4. 额外优化点

  • 统一数据源管理:如果两个分组的数据源从同一个总列表拆分而来,确保数据更新的原子性,避免动画过程中出现数据不一致。
  • 处理ScrollView滚动:若动画触发时ScrollView处于滚动状态,可临时禁用滚动或用ScrollViewReader配合scrollTo方法,在动画结束后将新位置的卡片滚动到可视区域。
  • Divider动画适配:当分组数量变化时,给Divider添加动画让整体过渡更流畅:
    Divider()
        .frame(minHeight: 3)
        .background(.gray)
        .padding(.bottom)
        .animation(.default, value: pinnedTasks.count)
    

内容的提问来源于stack exchange,提问作者esbenr

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 12:40:20