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

LazyHStack嵌套ScrollView中点击项滚动动画卡顿问题求助

解决嵌套LazyHStack的ScrollView平滑滚动与展开动画同步问题

问题描述

使用嵌套LazyHStack的ScrollView时,点击项的展开动画与滚动到视图内的操作不同步,部分可见的项无法同时完成展开和滚动,动画卡顿明显,尤其是项处于左右边缘部分可见状态时。期望实现选中项平滑展开、两侧项同步移位的连贯动画效果。

修复后的完整代码

import SwiftUI

struct CarouselItem: Identifiable, Equatable {
    private(set) var id: UUID = .init()
    var title: String
    var year: String
    var runtime: String
    var certification: String
    var posterImage: String
    var stillImage: String
}

struct CarousalItemView: View {
    let item: CarouselItem
    @Binding var selectedItem: CarouselItem?
    @Namespace private var namespace
    private var expanded: Bool {
        item.id == selectedItem?.id
    }
    
    var body: some View {
        VStack(alignment: .leading) {
            ZStack {
                Image(item.posterImage)
                    .resizable()
                    .scaledToFill()
                    .scaleEffect(expanded ? 1.5 : 1, anchor: .topLeading)
                    .frame(width: expanded ? 250 : 150, height: 200)
                    .matchedGeometryEffect(id: "image_\(item.id)", in: namespace, anchor: .topLeading)
                    .opacity(expanded ? 0 : 1)
                
                Image(item.stillImage)
                    .resizable()
                    .scaledToFill()
                    .frame(width: expanded ? 250 : 150, height: 200)
                    .matchedGeometryEffect(id: "image_\(item.id)", in: namespace, anchor: .topLeading)
                    .opacity(expanded ? 1 : 0)
                    .overlay(alignment: .leading) {
                        if expanded {
                            itemDetailsView()
                        }
                    }
            }
            .clipShape(RoundedRectangle(cornerRadius: 10))

            Text(item.title)
                .frame(height: 50)
        }
    }
    
    @ViewBuilder func itemDetailsView() -> some View {
        VStack(alignment: .leading, spacing: 15) {
            Spacer()
            Text(item.title)
                .font(.title2)
            HStack {
                Text("CBFC: " + item.certification)
                    .padding(.horizontal, 5)
                    .overlay {
                        Rectangle()
                            .stroke(.white, lineWidth: 1)
                    }
                Text(item.runtime)
                Text(item.year)
            }
            .shadow(color: .gray, radius: 5, y: 5)
            Button {

            } label: {
                Text("Trailer")
            }
            .buttonStyle(.borderedProminent)
            .tint(.gray)
            .padding(.bottom, 15)
        }
        .padding(.horizontal, 10)
    }
}

struct ContentView: View {
    var items: [CarouselItem] = [
        .init(title: "Oppenheimer", year: "2023", runtime: "3h 1m", certification: "UA", posterImage: "poster1", stillImage: "still1"),
        .init(title: "Barbie", year: "2023", runtime: "1h 54m", certification: "PG", posterImage: "poster2", stillImage: "still2"),
        .init(title: "The Martian", year: "2015", runtime: "2h 24m", certification: "UA", posterImage: "poster3", stillImage: "still3"),
        .init(title: "The Shawshank Redemption", year: "1994", runtime: "2h 22m", certification: "U", posterImage: "poster4", stillImage: "still4"),
        .init(title: "The Godfather", year: "1972", runtime: "2h 55m", certification: "UA", posterImage: "poster5", stillImage: "still5"),
        .init(title: "The Dark Knight", year: "2008", runtime: "2h 32m", certification: "UA", posterImage: "poster6", stillImage: "still6")
    ]
    @State var selectedItem: CarouselItem?
    var body: some View {
        VStack {
            HStack {
                Text("Popular films")
                    .font(.title2.bold())
                Spacer()
            }
            
            ScrollViewReader { proxy in
                ScrollView(.horizontal) {
                    LazyHStack(spacing: 20) {
                        ForEach(items, id: \.id) { item in
                            CarousalItemView(item: item, selectedItem: $selectedItem)
                                .frame(width: selectedItem?.id != item.id ? 150 : 250, height: 250)
                                .clipShape(RoundedRectangle(cornerRadius: 10))
                                .contentShape(Rectangle())
                                .id(item.id)
                                .onTapGesture {
                                    withAnimation(.easeInOut(duration: 0.3)) {
                                        if selectedItem?.id == item.id {
                                            selectedItem = nil
                                        } else {
                                            selectedItem = item
                                            proxy.scrollTo(item.id, anchor: .center)
                                        }
                                    }
                                }
                        }
                    }
                }
                .frame(height: 250)
                .scrollIndicators(.hidden)
                .clipped()
            }
            Spacer()
        }
        .padding()
        .background {
            Color(.darkGray).ignoresSafeArea()
        }
        .foregroundStyle(.white)
    }
}

核心改动说明

  • 统一动画控制:移除视图内所有独立的.animation修饰符,将所有动画逻辑集中到onTapGesture的withAnimation块中,避免多动画触发冲突,保证动画同步性
  • 修正滚动时机:确保选中状态更新后立即执行滚动操作,避免因状态延迟导致的滚动与展开不同步
  • 优化几何匹配:给matchedGeometryEffect的ID添加item唯一标识(image_\(item.id)),避免不同项之间的几何干扰,保证每个项的动画独立准确
  • 移除冗余代码:去掉不必要的DispatchQueue.main.async,onTapGesture默认在主队列执行,简化逻辑
  • 显式设置ID:为每个列表项显式添加.id(item.id),确保ScrollViewReader能精准定位到目标项

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 22:06:04