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

SwiftUI自定义Header高度获取卡顿无效,求动态适配方案

解决方案:动态自定义Header高度适配ScrollView

问题核心

你当前通过DispatchQueue.main.asyncAfter延迟获取Header高度的方式存在卡顿和不可靠问题,同时嵌套ScrollView+List的结构易引发布局冲突,导致高度同步失效。以下是针对性的优化方案:

优化方案一:实时同步Header高度

使用SwiftUI的PreferenceKey机制替代异步延迟,精准实时传递Header高度给List,彻底解决卡顿和高度不准的问题:

1. 定义PreferenceKey

struct HeaderHeightKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

2. 修改Header传递高度

替换原代码中获取高度的background(GeometryReader...)部分:

.background(
    GeometryReader { headerGeometry in
        Color.clear
            .preference(key: HeaderHeightKey.self, value: headerGeometry.size.height)
    }
)

3. 父视图接收并应用高度

在ZStack层级添加监听,实时更新List的顶部padding:

ZStack(alignment: .top) {
    // Header代码...
    // List代码...
}
.onPreferenceChange(HeaderHeightKey.self) { newHeight in
    headerHeight = newHeight
}

优化方案二:动态高度Header实现

要让Header随API内容自适应高度,只需保证Header内部元素使用自适应布局(如默认的VStack/HStack,不固定高度),结合上述PreferenceKey方案,布局变化时高度会自动同步给List。例如若API返回内容需在Header展示,直接将其加入Header的VStack即可,无需额外处理。

额外优化:移除嵌套ScrollView

原代码中List内部嵌套ScrollView会引发滚动冲突和性能损耗,直接使用List+LazyVStack即可:

List {
    LazyVStack(spacing: 20) {
        ForEach(dataViewModel.data?.leagues ?? [], id: \.id) { league in
            // league和match行代码...
        }
    }
    .padding(10)
    .listRowSeparator(.hidden)
    .padding(.top, 10)
    .background(Color.bgAccent)
    .listRowInsets(EdgeInsets())
}

完整优化代码示例

struct HeaderHeightKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

var body: some View {
    NavigationView {
        ZStack(alignment: .top) {
            // 自定义Header部分
            VStack(spacing: 24) {
                HStack(spacing: 14) {
                    balunImage
                        .resizable()
                        .scaledToFit()
                        .frame(height: 25)
                    
                    Spacer()
                    
                    Image(systemName: "clock")
                    Image(systemName: "magnifyingglass")
                    Image(systemName: "calendar")
                }.padding(.horizontal)
                
                ScrollView(.horizontal, showsIndicators: false) {
                    ScrollViewReader { scrollView in
                        HStack(spacing: 0) {
                            ForEach(-6...6, id: \.self) { dayOffset in
                                let date = Calendar.current.date(byAdding: .day, value: dayOffset, to: Date()) ?? Date()
                                let dateString = self.formatterString(for: dayOffset, with: date)
                                
                                Text(dateString)
                                    .padding(.horizontal, 20)
                                    .font(.system(size: 14))
                                    .fontWeight(.bold)
                                    .foregroundColor(dayOffset == selectedDayIndex ? Color.textAccent : Color.gray)
                                    .background(alignment: .bottom, content: {
                                        if dayOffset == selectedDayIndex {
                                            Capsule()
                                                .fill(Color.textAccent)
                                                .frame(height: 3)
                                                .padding(.horizontal, 14)
                                                .offset(y: 15)
                                                .matchedGeometryEffect(id: "ACTIVEDAY", in: namespace)
                                        }
                                    })
                                    .contentShape(Rectangle())
                                    .onTapGesture {
                                        func stringFromDate(_ date: Date) -> String {
                                            let dateFormatter = DateFormatter()
                                            dateFormatter.dateFormat = "yyyyMMdd"
                                            return dateFormatter.string(from: date)
                                        }
                                        
                                        let formattedDateString = stringFromDate(date)
                                        dataViewModel.fetchData(forDate: formattedDateString)
                                        
                                        withAnimation(.easeInOut(duration: 0.3)) {
                                            scrollView.scrollTo(dayOffset, anchor: .center)
                                            self.selectedDayIndex = dayOffset
                                        }
                                    }
                            }
                        }
                        .onAppear {
                            scrollView.scrollTo(selectedDayIndex, anchor: .center)
                        }
                        .padding(.bottom, 15)
                    }
                }
                .ignoresSafeArea()
            }
            .font(.system(size: 18))
            .foregroundColor(Color.textAccent)
            .frame(width: UIScreen.main.bounds.width)
            .padding(.bottom, 13)
            .background(Color.bgSecondaryAccent)
            .background(
                GeometryReader { headerGeometry in
                    Color.clear
                        .preference(key: HeaderHeightKey.self, value: headerGeometry.size.height)
                }
            )
            
            // List内容部分
            List {
                LazyVStack(spacing: 20) {
                    ForEach(dataViewModel.data?.leagues ?? [], id: \.id) { league in
                        VStack {
                            leagueHeader(league: league)
                            ForEach(league.matches, id: \.id) { match in
                                matchRow(match: match)
                            }
                        }
                        .foregroundStyle(Color.textAccent)
                        .background(Color.bgSecondaryAccent)
                        .cornerRadius(10)
                    }
                }
                .padding(10)
                .listRowSeparator(.hidden)
                .padding(.top, 10)
                .background(Color.bgAccent)
                .listRowInsets(EdgeInsets())
            }
            .padding(.top, headerHeight)
            .listStyle(.plain)
            .scrollIndicators(.hidden)
            .onAppear { UIRefreshControl.appearance().tintColor = .gray }
            .refreshable { dataViewModel.fetchData() }
            .onAppear {
                UIRefreshControl.appearance().tintColor = .gray
                dataViewModel.fetchData()
                timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { _ in
                    dataViewModel.fetchData()
                }
            }
            .onDisappear {
                timer?.invalidate()
                timer = nil
            }
        }
        .onPreferenceChange(HeaderHeightKey.self) { newHeight in
            headerHeight = newHeight
        }
    }
}

内容的提问来源于stack exchange,提问作者Michele de Vita

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 20:22:07