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

SwiftUI中List内动态设置ID时Hero动画失效问题

SwiftUI动态matchedGeometryEffect ID的Hero动画修复

问题描述

使用matchedGeometryEffect实现Hero动画时,动态绑定selectedItemInRowIndex作为ID时动画未按预期工作,但硬编码固定ID(如3)时仅对应行动画正常。

问题原因

  1. 所有列表行的绿色视图始终绑定各自的item.id作为matchedGeometryEffect的ID,当Overlay中的视图使用选中ID时,SwiftUI无法精准匹配唯一对应的源视图,导致动画混乱。
  2. List上的全局.animation修饰符会干扰matchedGeometryEffect的精细动画上下文,破坏动画同步。

修复方案

  1. 条件化绑定ID:列表行中的绿色视图仅在当前item被选中时,使用item.id作为共享ID;未选中时使用唯一的非共享ID,避免冲突。
  2. 移除全局动画:删除List上的全局动画修饰符,完全通过withAnimation包裹状态变更来控制动画。
  3. 优化Overlay交互:将黑色背景设置为全屏,确保点击任意区域可关闭详情。

修改后的完整代码

import SwiftUI

struct HeroAnimationTest: View {
    let items: [Item] = [.init(id: 1), .init(id: 2), .init(id: 3), .init(id: 4)]

    @State var selectedItemInRowIndex: Int? = nil
    @Namespace var namespace

    var body: some View {
        List {
            ForEach(items, id: \.id) { item in
                ItemListRow(namespace: namespace, 
                            item: item, 
                            selectedId: selectedItemInRowIndex) { tappedItem in
                    withAnimation(.spring()) {
                        selectedItemInRowIndex = tappedItem.id
                    }
                }
            }
            .listRowSeparator(.hidden)
            .listRowBackground(Color.clear)
            .listRowInsets(EdgeInsets(h: 16, v: 8))
        }
        .scrollIndicators(.never)
        .listStyle(.plain)
        .overlay {
            if let selectedId = selectedItemInRowIndex {
                largeGreen(selectedId: selectedId)
            }
        }
    }

    @ViewBuilder
    func largeGreen(selectedId: Int) -> some View {
        ZStack {
            Color.black
                .ignoresSafeArea()
                .onTapGesture {
                    withAnimation(.spring()) {
                        selectedItemInRowIndex = nil
                    }
                }
            Color.green
                .frame(width: 200, height: 400)
                .matchedGeometryEffect(id: selectedId, in: namespace)
            Text("ID -> \(selectedId)")
                .foregroundColor(.white)
                .font(.headline)
        }
    }
}

struct HeroAnimationTest_Previews: PreviewProvider {
    static var previews: some View {
        HeroAnimationTest()
    }
}

struct Item {
    let id: Int
}

struct ItemListRow: View {
    let namespace: Namespace.ID
    let item: Item
    let selectedId: Int?
    let onGreenTap: (Item) -> Void

    var body: some View {
        HStack {
            Text("ID -> \(item.id)")
            Color.green
                .frame(width: 100, height: 40)
                .matchedGeometryEffect(
                    id: selectedId == item.id ? item.id : "unselected-\(item.id)",
                    in: namespace
                )
                .onTapGesture {
                    onGreenTap(item)
                }
            Color.yellow
                .frame(width: 100, height: 40)
        }
    }
}

效果说明

修改后,点击任意列表行的绿色视图时,该视图会平滑过渡到Overlay中的大尺寸绿色视图;点击黑色背景时,大视图会动画回到原列表行位置,所有行均能正常触发Hero动画。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 14:35:39