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

相关推荐
海的天空166122 分钟前
Flutter旧版本升级-> Android 配置、iOS配置
android·flutter·ios
kymjs张涛16 小时前
零一开源|前沿技术周刊 #13
ios·harmonyos·apple
源码哥_博纳软云20 小时前
JAVA国际版多商户运营版商城系统源码多商户社交电商系统源码支持Android+IOS+H5
android·java·ios·微信·微信小程序·小程序·uni-app
2501_915106321 天前
iOS混淆工具实战 金融支付类 App 的安全防护与合规落地
android·ios·小程序·https·uni-app·iphone·webview
I烟雨云渊T1 天前
iOS 数据持久化
macos·ios·cocoa
從南走到北1 天前
JAVA国际版东郊到家同城按摩服务美容美发私教到店服务系统源码支持Android+IOS+H5
android·java·开发语言·ios·微信·微信小程序·小程序
亿刀1 天前
WireGuard概述
ios
YungFan1 天前
iOS26适配指南之UIViewController
ios·swift
Magnetic_h2 天前
【iOS】SDWebImage第三方库源码学习笔记
笔记·学习·ios·objective-c·cocoa
TellMeha2 天前
uniapp打包app关于获取本机相机、相册、麦克风等权限问题(ios第一次安装权限列表里没有对应权限问题)
ios·uni-app