You need to enable JavaScript to run this app. SwiftUI如何检测ScrollView内子视图的位置变化实现单视频自动播放
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI如何检测ScrollView内子视图的位置变化实现单视频自动播放

解决方案

以下是针对SwiftUI中ScrollView多视频单播需求的可落地实现方案:

核心实现逻辑:监听实时滚动偏移

通过SwiftUI的PreferenceKey可以无侵入获取ScrollView的实时滚动位置,无需桥接UIKit,兼容性最好。

步骤1:定义滚动偏移监听的PreferenceKey

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

步骤2:给ScrollView绑定偏移监听

在ScrollView的最上层添加透明背景的GeometryReader,将滚动偏移实时同步到状态变量,滚动变化时直接触发视频可见性判断:

@State private var scrollOffset: CGFloat = 0
// 防抖用的任务变量,避免滚动过程中频繁触发播放
@State private var playCheckTask: DispatchWorkItem?

var body: some View {
    ScrollView {
        // 滚动偏移监听层
        GeometryReader { proxy in
            Color.clear
                .preference(key: ScrollOffsetPreferenceKey.self, value: proxy.frame(in: .global).minY)
        }
        // 视频列表建议用LazyVStack优化性能
        LazyVStack(spacing: 16) {
            ForEach(videoItems) { item in
                VideoCellView(postID: item.id, avPlayer: item.player, scrollOffset: scrollOffset)
            }
        }
    }
    .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
        scrollOffset = offset
        // 防抖处理:滚动停止300ms后再执行播放判断,减少性能消耗
        playCheckTask?.cancel()
        let task = DispatchWorkItem {
            checkVisibleVideo()
        }
        playCheckTask = task
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: task)
    }
}

步骤3:统一管理播放器状态,实现单播逻辑

创建全局播放器管理类,避免多个视频同时播放:

class PlayerManager: ObservableObject {
    static let shared = PlayerManager()
    // 当前正在播放的视频ID
    @Published var currentPlayingID: String?
    
    func playSingleVideo(id: String, player: AVPlayer) {
        // 先暂停上一个播放的视频
        if let oldID = currentPlayingID, oldID != id {
            NotificationCenter.default.post(
                name: NSNotification.Name("pauseOldVideo"), 
                object: oldID
            )
        }
        player.play()
        currentPlayingID = id
    }
}

步骤4:优化视频Cell的可见性判断

你原有的位置判断逻辑不需要写在onAppear中,改为监听全局滚动偏移变化即可,同时适配不同机型的屏幕尺寸,不要写死固定数值:

struct VideoCellView: View {
    let postID: String
    let avPlayer: AVPlayer
    @EnvironmentObject var playerManager: PlayerManager
    // 从父视图传入的滚动偏移
    let scrollOffset: CGFloat
    
    var body: some View {
        GeometryReader { geo in
            VideoPlayer(player: avPlayer)
                .onChange(of: scrollOffset) { _ in
                    let globalMidY = geo.frame(in: .global).midY
                    // 动态计算屏幕中间可视区域,适配所有机型
                    let screenHeight = UIScreen.main.bounds.height
                    let visibleRange = (screenHeight * 0.2)...(screenHeight * 0.8)
                    
                    if visibleRange.contains(globalMidY) {
                        playerManager.playSingleVideo(id: postID, player: avPlayer)
                        print("Global center: \(geo.frame(in: .global).midX) x \(globalMidY)")
                        print("Video to play: \(postID)")
                    }
                }
                // 监听暂停通知,停止非当前播放的视频
                .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("pauseOldVideo"))) { notification in
                    guard let pauseID = notification.object as? String, pauseID != postID else { return }
                    avPlayer.pause()
                }
        }
        .frame(height: 300) // 替换为你的视频实际高度
    }
}

可选方案:监听拖拽事件

如果需要捕获拖拽开始/结束的生命周期,可以给ScrollView添加同步手势,不会影响原有滚动行为:

ScrollView {
    // 原有内容
}
.simultaneousGesture(
    DragGesture()
        .onChanged { _ in
            // 拖拽开始/滚动中触发的逻辑
        }
        .onEnded { _ in
            // 拖拽结束触发的逻辑
        }
)

注意事项

  • 视频数量较多时必须使用LazyVStack,自动复用视图避免内存溢出
  • 视频滑出可视区域后要主动调用avPlayer.pause()释放资源
  • 固定的200/800数值替换为动态屏幕比例计算,避免小屏/大屏机型适配问题

内容的提问来源于stack exchange,提问作者savante

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.13 09:15:03