SwiftUI 打造 TikTok 风格的滑动短视频播放器

结合 ScrollView + LazyVStack + AVPlayer,30 行核心代码即可完成「滑到哪儿播到哪儿」的沉浸式体验。


本文参考Yutube上的一个Demo,将原视频的视频播放和暂停逻辑做了改动,利用本地视频,实现类似TikTok的视频秀效果

项目结构速览

文件 作用
TikTokView.swift 顶层容器,负责垂直分页滚动与当前播放状态
TikTokContentView.swift 单条视频卡片,内部持有独立 AVPlayer
LightweightVideoPlayer.swift 极简 UIViewRepresentable,只渲染视频层
Models.swift FeedResponse 数据模型 & 本地假数据
Assets/ mp4 示例视频与自定义字体 TikTokIcons.ttf

1. TikTokView:一屏一视频的分页列表

swift 复制代码
ScrollView(showsIndicators: false) {
    LazyVStack(spacing: 0) {
        ForEach(feedData) { feed in
            TikTokContentView(feed: feed,
                              currentPlayingID: $currentPlayingID)
                .id(feed.id)                 // ✨ 绑定唯一 id
        }
    }
    .scrollTargetLayout()
}
.scrollTargetBehavior(.paging)          // ✨ 分页滚动
.scrollPosition(id: $scrollPosition)    // ✨ 捕获当前可见 id
.onChange(of: scrollPosition) { _, newID in
    currentPlayingID = newID            // 告诉子组件该谁播放
}
  • 分页滚动scrollTargetBehavior(.paging) 让每次滑动都停在整页。
  • 当前页识别scrollPosition(id:) 自动把最居中的 cell 的 id 写入 scrollPosition 绑定。
  • 播放指令下发onChange 将最新 id 存入 currentPlayingID,所有子组件都能感知。

这样就避免了手动计算可见区域,也无需监听 GeometryReader


2. TikTokContentView:每条视频自带一个 AVPlayer

swift 复制代码
@State private var player: AVPlayer
private var isActive: Bool { currentPlayingID == feed.id }

init(feed: FeedResponse,
     currentPlayingID: Binding<String?>) {
    _currentPlayingID = currentPlayingID
    let url = Bundle.main.url(forResource: feed.videoUrl,
                              withExtension: "mp4")!
    _player = State(initialValue: AVPlayer(url: url))
}
  • 初始化阶段即 为每个视频创建独立 AVPlayer ,并缓存在 @State;这样切走再返回时能秒播,且无需全局单例。

播放 & 暂停的核心逻辑

swift 复制代码
private func syncPlayback() {
    if isActive {
        player.play()     // 当前页:播放
    } else {
        player.pause()    // 其它页:暂停
    }
}

.onAppear { syncPlayback() }         // 首次出现
.onChange(of: currentPlayingID) { _ in syncPlayback() }
.onDisappear {                       // 离屏:停+复位
    player.pause()
    player.seek(to: .zero)
}
  1. 出现时 :判断自己是不是当前页,决定是否立刻 play()
  2. 全局 id 变化 :再次比对,若失去焦点则 pause()
  3. 完全离屏:保证下次出现从 0 秒开始。

整个逻辑只依赖一个 currentPlayingID,干净、可测试,无需 KVO 监听滚动偏移。


3. LightweightVideoPlayer:极简 UIKit 封装

swift 复制代码
final class PlayerLayerView: UIView {
    override class var layerClass: AnyClass { AVPlayerLayer.self }
    var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
}

struct LightweightVideoPlayer: UIViewRepresentable {
    let player: AVPlayer
    func makeUIView(context: Context) -> PlayerLayerView {
        let v = PlayerLayerView()
        v.playerLayer.videoGravity = .resizeAspectFill   // 铺满
        v.playerLayer.player = player
        return v
    }
    func updateUIView(_ uiView: PlayerLayerView, context: Context) {}
}
  • 只有 15 行 :去掉 AVPlayerViewController 冗余 UI。
  • containerRelativeFrame:在父级强制铺满,全屏显示。

4. 性能小结

优化点 说明
LazyVStack 只实例化可见行,减少内存
独立 AVPlayer 避免频繁换源;复用同一 AVPlayer 反而要处理时许状态机
.scrollTargetBehavior(.paging) 系统级分页,动画跟手且省心
onDisappear 重置 释放解码管线,防止后台继续占用 GPU

5. 可以做得更好的地方

  1. 预加载下一条视频 :滑动前提前 prepare,体验更丝滑。
  2. 远程流媒体 :示例用了本地 mp4,生产可接入 HLS。
  3. 封面 & 首帧:未播放时显示静态封面,减少黑屏闪烁。
  4. 全局静音切换:结合系统音量/静音键。
  5. 点赞动画 :借助 MatchedGeometryEffect 或自定义 CAEmitterLayer

下载 DEMO 源码

点此下载完整 SwiftUI 示例项目

相关推荐
namehu21 分钟前
搞定 iOS App 测试包分发,也就这么简单!😎
前端·ios·app
用户094 小时前
如何避免写垃圾代码:iOS开发篇
ios·swiftui·swift
HarderCoder18 小时前
iOS 知识积累第一弹:从 struct 到 APP 生命周期的全景复盘
ios
叽哥1 天前
Flutter Riverpod上手指南
android·flutter·ios
用户092 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan2 天前
iOS26适配指南之UIColor
ios·swift
权咚3 天前
阿权的开发经验小集
git·ios·xcode
用户093 天前
TipKit与CloudKit同步完全指南
ios·swift
法的空间3 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918413 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview