SwiftUI带滚动监听的ScrollView无法维持内容状态问题排查
问题描述
为监听滚动开始和结束事件,我基于自定义的ScrollViewOffsetReader封装了TrackableScrollView组件,代码如下:
struct TrackableScrollView<Content>: View where Content: View { let axes: Axis.Set let showIndicators: Bool private let onScrollingStarted: () -> Void private let onScrollingFinished: () -> Void let content: Content public init(_ axes: Axis.Set = .vertical, showIndicators: Bool = true, @ViewBuilder content: () -> Content, onScrollingStarted: @escaping () -> Void = {}, onScrollingFinished: @escaping () -> Void = {}) { self.axes = axes self.showIndicators = showIndicators self.content = content() self.onScrollingStarted = onScrollingStarted self.onScrollingFinished = onScrollingFinished } public var body: some View { ScrollView(axes, showsIndicators: showIndicators) { ZStack { ScrollViewOffsetReader(onScrollingStarted: { onScrollingStarted() }, onScrollingFinished: { onScrollingFinished() }) LazyVStack { self.content } } } } }
组件整体运行正常,但存在问题:ScrollView最顶部的LottieAnimationView已设置为仅播放一次,但每次滚动时动画都会重置并重新播放。移除onScrollingStarted和onScrollingFinished回调后问题消失,但这两个回调是视图必需功能,需解决该冲突。
解决方案
问题根源是滚动回调触发时,ScrollViewOffsetReader的实现可能导致视图树频繁重绘,进而让LazyVStack重新初始化内部视图(包括LottieAnimationView)。可通过以下方式解决:
1. 让LottieAnimationView持久化播放状态
将Lottie的播放状态封装到可观察对象中,避免视图重绘时重置:
class LottiePlayerState: ObservableObject { var hasPlayed = false } struct PersistentLottieView: View { @StateObject private var playState = LottiePlayerState() var body: some View { LottieAnimationView(name: "your_animation_file") .onAppear { guard !playState.hasPlayed else { return } self.play() // 调用Lottie的播放方法 playState.hasPlayed = true } } }
通过hasPlayed标记记录播放状态,即使视图被意外重建,也不会重复播放动画。
2. 优化TrackableScrollView的视图结构
修改TrackableScrollView,避免因ScrollViewOffsetReader导致内容视图频繁重绘:
struct TrackableScrollView<Content>: View where Content: View { let axes: Axis.Set let showIndicators: Bool private let onScrollingStarted: () -> Void private let onScrollingFinished: () -> Void @ViewBuilder private let content: () -> Content public init(_ axes: Axis.Set = .vertical, showIndicators: Bool = true, @ViewBuilder content: @escaping () -> Content, onScrollingStarted: @escaping () -> Void = {}, onScrollingFinished: @escaping () -> Void = {}) { self.axes = axes self.showIndicators = showIndicators self.content = content self.onScrollingStarted = onScrollingStarted self.onScrollingFinished = onScrollingFinished } public var body: some View { ScrollView(axes, showsIndicators: showIndicators) { LazyVStack { content() } .background( ScrollViewOffsetReader( onScrollingStarted: onScrollingStarted, onScrollingFinished: onScrollingFinished ) ) } } }
关键修改:
- 将
content改为@escaping @ViewBuilder () -> Content,让SwiftUI按需创建并缓存内容视图,而非初始化时提前创建。 - 将
ScrollViewOffsetReader移到LazyVStack的背景层,分离滚动监听逻辑与内容视图,减少对内容视图重绘的触发。
3. 优化ScrollViewOffsetReader的回调触发逻辑
如果ScrollViewOffsetReader内部通过频繁的offset变化触发回调,会导致过多状态更新。可添加防抖逻辑:
- 滚动开始时,仅在从静止状态切换为滚动状态时触发一次
onScrollingStarted,而非重复触发。 - 滚动结束时,使用Timer延迟触发
onScrollingFinished(例如延迟0.2秒),避免微小滚动就触发回调,减少不必要的视图重绘。
内容的提问来源于stack exchange,提问作者Paras Gorasiya
相关产品推荐
相关产品推荐

