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
相关产品推荐
相关产品推荐

