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

SwiftUI实现下拉时Header置顶并拉伸效果的技术求助

SwiftUI实现下拉ScrollView时顶部Header固定且图片可拉伸的交互效果

需求:复刻Gentler Streaks应用的交互——下拉ScrollView时,顶部包含两张图片的Header固定在视图顶部,同时背景图片可随下拉动作拉伸。

当前问题:Header随ScrollView一起滚动,下拉时没有图片拉伸效果。

以下是修改后的完整代码,实现所需交互:

import SwiftUI
import MapKit

struct PredatorDetail: View {
    let predator: ApexPredator
    @State var position: MapCameraPosition
    // 记录滚动偏移量
    @State private var scrollOffset: CGFloat = 0
    // Header基础高度
    private let baseHeaderHeight: CGFloat = 280
    
    var body: some View {
        GeometryReader { rootGeo in
            ZStack(alignment: .top) {
                // 固定在顶部的Header
                HeaderView(predator: predator, scrollOffset: scrollOffset, baseHeight: baseHeaderHeight, screenWidth: rootGeo.size.width)
                
                ScrollView {
                    LazyVStack(alignment: .leading, spacing: 0) {
                        // 占位空间,避免内容被Header遮挡
                        Color.clear.frame(height: baseHeaderHeight)
                        
                        //dino name
                        Text(predator.name)
                            .font(.largeTitle)
                        
                        //current location
                        NavigationLink {
                            PredatorMap(position: .camera(MapCamera(centerCoordinate: predator.location, distance: 1000, heading: 250, pitch: 80)))
                        } label: {
                            Map(position: $position) {
                                Annotation(predator.name, coordinate: predator.location) {
                                    Image(systemName: "mappin.and.ellipse")
                                        .font(.largeTitle)
                                        .imageScale(.large)
                                        .symbolEffect(.pulse)
                                }
                                .annotationTitles(.hidden)
                            }
                            .frame(height: 125)
                            .overlay(alignment: .trailing) {
                                Image(systemName: "greaterthan")
                                    .imageScale(.large)
                                    .font(.title3)
                                    .padding(.trailing, 5)
                            }
                            .overlay(alignment: .topLeading) {
                                Text("Current Location")
                                    .padding([.leading, .bottom], 5)
                                    .padding(.trailing, 8)
                                    .background(.black.opacity(0.7))
                                    .clipShape(.rect(bottomTrailingRadius:15))
                            }
                            .clipShape(.rect(cornerRadius: 15))
                        }
                        
                        // apears in
                        Text("Appears In:")
                            .font(.title3)
                        
                        ForEach(predator.movies, id: \.self) { movie in
                            Text("•" + movie)
                                .font(.subheadline)
                        }
                        // move momens
                        Text("Movie Moments:")
                            .font(.title)
                            .padding(.top, 15)
                        
                        ForEach(predator.movieScenes) { scene in
                            Text(scene.movie)
                                .font(.title2)
                                .padding(.vertical, 1)
                            Text(scene.sceneDescription)
                                .padding(.bottom, 15)
                        }
                        Text("Read More")
                            .font(.caption)
                        
                        Link(predator.link, destination: URL(string: predator.link)!)
                            .font(.caption)
                            .foregroundStyle(.blue)
                    }
                    .padding()
                    .padding(.bottom, 30)
                    .frame(width: rootGeo.size.width, alignment: .leading)
                    // 监听滚动偏移
                    .background(
                        GeometryReader { geo in
                            Color.clear
                                .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollView")).minY)
                        }
                    )
                }
                .coordinateSpace(name: "scrollView")
                .ignoresSafeArea()
            }
            .onPreferenceChange(ScrollOffsetKey.self) { value in
                // 只处理下拉的偏移量(value > 0表示下拉)
                scrollOffset = max(value, 0)
            }
        }
        .toolbarBackground(.automatic)
    }
}

// 自定义Header视图
struct HeaderView: View {
    let predator: ApexPredator
    let scrollOffset: CGFloat
    let baseHeight: CGFloat
    let screenWidth: CGFloat
    
    var body: some View {
        ZStack (alignment: .bottomTrailing) {
            // 背景图片,随下拉拉伸
            Image(predator.type.rawValue)
                .resizable()
                .scaledToFill()
                // 计算拉伸后的高度:基础高度 + 滚动偏移量
                .frame(height: baseHeight + scrollOffset)
                // 保持宽度充满屏幕,拉伸时居中显示
                .frame(width: screenWidth)
                .overlay {
                    LinearGradient(stops: [Gradient.Stop(color: .clear, location: 0.8), Gradient.Stop(color: .black, location: 1)], startPoint: .top, endPoint: .bottom)
                }
                .clipped()
            
            // 恐龙图片,固定位置(下拉时仅背景图拉伸)
            Image(predator.image)
                .resizable()
                .scaledToFit()
                .frame(width: screenWidth/1.5, height: baseHeight * 0.7)
                .scaleEffect(x: -1)
                .shadow(color: .black, radius: 7)
                // 固定在Header底部,不随滚动偏移移动
                .offset(y: baseHeight * 0.1)
        }
        // 固定Header的基础高度,下拉时高度动态增加
        .frame(height: baseHeight + scrollOffset)
    }
}

// 自定义PreferenceKey监听滚动偏移
struct ScrollOffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

#Preview {
    PredatorDetail(predator: Predators().apexPredators[10], position: .camera(MapCamera(centerCoordinate: Predators().apexPredators[10].location, distance: 30000)))
        .preferredColorScheme(.dark)
}

关键实现说明:

  • 滚动偏移监听:通过GeometryReader和自定义PreferenceKey获取ScrollView的滚动偏移量,仅处理下拉(偏移量为正)的情况。
  • Header固定:将Header放在ZStack的顶部层级,独立于ScrollView,实现固定效果。
  • 图片拉伸:背景图片的高度随滚动偏移量动态增加,使用scaledToFill保证图片拉伸时充满屏幕,同时保持居中显示。
  • 内容占位:在ScrollView最顶部添加与Header基础高度一致的透明占位,避免内容被Header遮挡。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 16:23:15