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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 14:40:24