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 示例项目

相关推荐
林鸿群3 分钟前
Apple M3 MacOS arm64 编译QGroundControl5.0.8(base on Qt 6.8.3)
macos·ios·qgc·qgroundcontrol
2501_916013741 小时前
iOS 推送开发完整指南,APNs 配置、证书申请、远程推送实现与上架调试经验分享
android·ios·小程序·https·uni-app·iphone·webview
2501_915909066 小时前
HTML5 与 HTTPS,页面能力、必要性、常见问题与实战排查
前端·ios·小程序·https·uni-app·iphone·html5
2501_9151063210 小时前
JavaScript编程工具有哪些?老前端的实用工具清单与经验分享
开发语言·前端·javascript·ios·小程序·uni-app·iphone
2501_9160137412 小时前
iOS 上架 App 全流程实战,应用打包、ipa 上传、App Store 审核与工具组合最佳实践
android·ios·小程序·https·uni-app·iphone·webview
2501_9151063212 小时前
iOS 26 能耗监测全景,Adaptive Power、新电池视图
android·macos·ios·小程序·uni-app·cocoa·iphone
Geek 研究僧16 小时前
iPhone 17 Pro Max 的影像升级全解:从长焦、前置聊到 ProRes RAW
图像处理·ios·iphone·影像
鹏多多21 小时前
flutter-切换状态显示不同组件10种实现方案全解析
android·前端·ios
jh_cao1 天前
(4)SwiftUI 基础(第四篇)
ios·swiftui·swift