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

SwiftUI 实现上滑滚动时文本移入头部固定同行效果问询

解决方案

核心调整说明

  • 拆分文本布局:将固定显示的Text("Hello")放在ScrollView外层,保证滚动全程固定在头部位置
  • 同步滚动状态:将滚动偏移量的监听结果直接绑定到isHStacked状态,触发两个文本的布局切换
  • 新增动画过渡:布局切换时添加系统默认动画,避免交互生硬跳变
  • 修复语法问题:给未初始化的状态变量添加默认值,解决编译报错

完整实现代码

首先是无需修改的监听偏移量的PreferenceKey:

struct ViewOffsetKey: PreferenceKey {
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}

调整后的TagLineView,适配两种布局状态:

struct TagLineView: View {
    @Binding var isHStacked: Bool
    var body: some View {
        if isHStacked {
            // 滚动到阈值后,只显示Welcome和外部的Hello同行
            Text("Welcome!")
                .font(.title)
        } else {
            // 初始状态,显示Hello+换行+Welcome
            VStack(alignment: .leading, spacing: 4) {
                Text("Hello")
                    .font(.title)
                    .fontWeight(.bold)
                Text("Welcome!")
                    .font(.title)
            }
        }
    }
}

调整后的主视图ContentView,外层固定Hello,内部监听滚动切换状态:

struct ContentView: View {
    // 给状态变量添加默认初始值
    @State var isHStacked: Bool = false
    
    var body: some View {
        NavigationView {
            VStack(spacing: 0) {
                // 外层固定的头部区域,Hello始终固定在这里
                HStack {
                    Text("Hello")
                        .font(.title)
                        .fontWeight(.bold)
                    // 滚动到阈值后显示Welcome,和Hello同行
                    if isHStacked {
                        Text("Welcome!")
                            .font(.title)
                    }
                    Spacer()
                }
                .padding(.horizontal)
                .padding(.vertical, 12)
                
                ScrollView {
                    VStack(alignment:.leading, spacing: 0) {
                        // 内部的标题区域,初始状态显示完整两行标题
                        TagLineView(isHStacked: $isHStacked)
                            .padding()
                            .opacity(isHStacked ? 0 : 1) // 切换后隐藏内部标题
                        
                        ZStack(alignment:.leading) {
                            Color(red: 0.937, green: 0.937, blue: 0.937)
                                .edgesIgnoringSafeArea(.all)
                                .cornerRadius(30.0)
                            VStack(alignment: .leading, spacing: 20) {
                                Text("Popular")
                                    .font(.custom("PlayfairDisplay-Bold", size: 24))
                                    .padding(.leading,27)
                                    .padding(.top,20)
                                
                                ScrollView (.horizontal, showsIndicators: false) {
                                    HStack (spacing: 0) {
                                        ForEach(0 ..< 4) { i in
                                            ProductCardView(image: Image("chair_\(4-i)"), size: 180)
                                        }
                                        .padding(.leading)
                                    }
                                }
                                .padding(.bottom)
                                
                                Text("Best")
                                    .font(.custom("PlayfairDisplay-Bold", size: 24))
                                    .padding(.horizontal)
                                
                                ScrollView (.horizontal, showsIndicators: false) {
                                    HStack (spacing: 0) {
                                        ForEach(0 ..< 4) { i in
                                            ProductCardView(image: Image("chair_\(4-i)"), size: 180)
                                        }
                                        .padding(.leading)
                                    }
                                }
                                
                                Text("Best")
                                    .font(.custom("PlayfairDisplay-Bold", size: 24))
                                    .padding(.horizontal)
                                
                                ScrollView (.horizontal, showsIndicators: false) {
                                    HStack (spacing: 0) {
                                        ForEach(0 ..< 4) { i in
                                            ProductCardView(image: Image("chair_\(4-i)"), size: 180)
                                        }
                                        .padding(.leading)
                                    }
                                }
                                .padding(.bottom, 30)
                            }
                        }
                        .background(GeometryReader {
                            Color.clear.preference(key: ViewOffsetKey.self,
                                                   value: -$0.frame(in: .named("scroll")).origin.y)
                        })
                        .onPreferenceChange(ViewOffsetKey.self) { offset in
                            // 滚动偏移超过阈值时切换状态,加动画过渡
                            withAnimation {
                                isHStacked = offset > 5
                            }
                        }
                    }
                }
                .coordinateSpace(name: "scroll")
            }
            .navigationBarHidden(true)
        }
    }
}

补充ProductCardView占位实现,可直接运行测试:

struct ProductCardView: View {
    let image: Image
    let size: CGFloat
    var body: some View {
        RoundedRectangle(cornerRadius: 16)
            .fill(Color.gray.opacity(0.3))
            .frame(width: size, height: size)
            .padding(.trailing, 16)
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 09:18:00