SwiftUI中获取ScrollView最高可见视图遇onDisappear未触发问题
解决ScrollView中LazyHStack视图onDisappear不触发的问题
我之前也碰到过类似的情况,LazyHStack的onDisappear在ScrollView场景下经常不太靠谱,再加上你把ScrollViewReader放在了ScrollView内部,这会进一步影响视图可见性的追踪逻辑。咱们一步步来修复这个问题:
核心问题分析
- ScrollViewReader的位置错误:它应该包裹整个ScrollView,而不是作为ScrollView的子视图,这样才能正确关联滚动容器和内部的懒加载视图。
- LazyHStack的onDisappear触发时机:SwiftUI的懒加载视图只有在完全移出渲染范围且系统需要回收内存时才会触发
onDisappear,这意味着轻微滚动可能不会触发,完全满足不了实时更新可见视图集合的需求。
修复方案
我们改用GeometryReader来检测每个BarView的可见性,同时调整ScrollViewReader的位置,确保能准确追踪滚动状态:
private var highestVisibleAmount: Double { visibleDates .compactMap { viewModel.transactions[$0] } .map { $0.map{ $0.amount }.reduce(0, +) } .max() ?? 0 } @State var visibleDates: Set<Date> = [] @State private var scrollOffset: CGFloat = 0 var body: some View { ScrollViewReader { reader in ScrollView(Axis.Set.horizontal, showsIndicators: false) { LazyHStack(alignment: .bottom, spacing: 1) { ForEach(viewModel.transactions.keys.sorted(), id: \.self) { date in let transactions = viewModel.transactions[date] ?? [] BarView(viewModel: BarView.ViewModel(transactions: transactions, highestVisibleAmount: highestVisibleAmount)) .frame(width: viewModel.segmentLength) .background( GeometryReader { geo in Color.clear .onAppear { checkVisibility(geo: geo, date: date) } .onChange(of: scrollOffset) { _ in checkVisibility(geo: geo, date: date) } } ) } } // 捕获滚动偏移量 .background( GeometryReader { geo in Color.clear .preference(key: ScrollOffsetPreferenceKey.self, value: geo.frame(in: .named("scroll")).minX) } ) } .coordinateSpace(name: "scroll") .frame(height: viewModel.chartHeight) .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in scrollOffset = offset } } } // 自定义PreferenceKey用于获取滚动偏移 private struct ScrollOffsetPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {} } // 检查视图是否可见的逻辑 private func checkVisibility(geo: GeometryProxy, date: Date) { let viewFrame = geo.frame(in: .global) let screenBounds = UIScreen.main.bounds // 判断视图是否与屏幕范围有交集(部分可见也算) let isVisible = viewFrame.intersects(screenBounds) if isVisible { visibleDates.insert(date) } else { visibleDates.remove(date) } }
补充说明
- 滚动偏移监听:通过自定义
ScrollOffsetPreferenceKey可以实时获取ScrollView的滚动位置,结合GeometryReader的frame判断,能准确检测每个BarView是否在屏幕可见范围内。 - 可见性判断逻辑:使用
frame.intersects来判断视图是否与屏幕范围有交集,这样即使视图部分可见也会被标记为可见,更符合柱状图的需求。 - ScrollViewReader的位置:现在它包裹整个ScrollView,后续如果需要实现滚动到指定柱状图的功能,也能正常工作。
你还可以根据实际需求调整可见性的判断阈值,比如要求视图至少一半可见才标记为可见,只需要修改intersects的判断逻辑即可。
内容的提问来源于stack exchange,提问作者WikipediaBrown
相关产品推荐
相关产品推荐

