扬庆の博客

SwiftUI-ReelsLayout 抖音

字数统计: 970阅读时长: 5 min
2024/03/20 Share

SwiftUI -ReelsLayout App

YouTube Link address:

效果图
ui
类似于tiktok 播放页面, 滑动切换
  • AVKit UIViewControllerRepresentable

  • makeUIViewController updateUIViewController

  • OffsetKey: PreferenceKey

  • .preference .onPreferenceChange

  • playPause(:)

页面结构

  • HomeView

    • Scrollview

      • Overlay ( 其他控件 不跟着 scrollView 滚动 )

          • 点赞双击的红心(带偏移动画)
      • LazyVStack (记住是 LazyVStack)

        • ReelView

        • ReelView

        • ReelView …

          • CustomVideoPlayer

            • Overlay ( 界面具体元素 [ 点赞 评论 私信 更多 ] )

            • onTapGesture(count: 2 …)

              • likedCounter数组 isLiked = true 触发点赞动画效果
            • onAppear

              • 新建 player [ player = queue ]
              • AVPlayerLooper ( 循环播放 )
              • 播放完成之后 player.seek(to: .zero) 重头开始
            • onDisappear

              • player = nil
      • overlay ( scrollView 界面上其他元素 [ Reels标题, )

        • 标题 Reels
        • Camera 图标📷


按照页面page 样式, 竖屏滑动

scrollTargetBehavior.png

ReelVIew 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//

// ReelView.swift

// ReelsLayout

//

// Created by 杨庆 on 2024/3/11.

//



import SwiftUI

import AVKit



/// Reel View

struct ReelView: View {
@Binding var reel: Reel
@Binding var likedCounter: [Like]
var size: CGSize
var safeArea: EdgeInsets

/// View Properties
@State private var player: AVPlayer?

var body: some View {
GeometryReader {
let rect = $0.frame(in: .scrollView(axis: .vertical))
/// Custom Video Player
CustomVideoPlayer(player: $player)
/// Offset Updates
.preference(key: OffsetKey.self, value: rect)
.onPreferenceChange(OffsetKey.self, perform: { value in
playPause(value)
})
.overlay(alignment: .bottom) {
ReelDetailsView(reel: reel)
}
/// Double Tap Like Animation
.onTapGesture(count: 2, perform: { position in
let id = UUID()
likedCounter.append(.init(id: id, tappedRect: position, isAnimated: false))
withAnimation(.snappy(duration: 1.2), completionCriteria: .logicallyComplete) {
if let index = likedCounter.firstIndex(where: { $0.id == id }) {
likedCounter[index].isAnimated = true
}
} completion: {
/// Removing Like, Once it's Finished
likedCounter.removeAll(where: { $0.id == id })
}
/// Liking the Reel
reel.isLiked = true
})
/// Creating Player
.onAppear {
print(safeArea.bottom)
guard player == nil else { return }
guard let bundleID = Bundle.main.path(forResource: reel.videoID, ofType: "mp4") else { return }
let videoURL = URL(filePath: bundleID)
player = AVPlayer(url: videoURL)
}
/// Clearing Player
.onDisappear {
print("onDisppear ----- ")
player = nil
}
}
.frame(width: size.width, height: size.height)
}


/// Play / Pause Action

func playPause(_ rect: CGRect) {

print("minY -----\(rect.minY)")
if -rect.minY < (rect.height * 0.5) && rect.minY < (rect.height * 0.5) {
player?.play()
} else {
player?.pause()
}
if rect.minY >= size.height || -rect.minY >= size.height {
player?.seek(to: .zero)
}
}
/// Reel Details & Controls
@ViewBuilder
func ReelDetailsView(reel: Reel) -> some View {
HStack(alignment: .bottom, spacing: 10) {
VStack(alignment: .leading, spacing: 8, content: {
HStack(spacing: 10) {
Image(systemName: "person.circle.fill")
.font(.largeTitle)
Text(reel.authorName)
.font(.callout)
.lineLimit(1)
}
.foregroundStyle(.white)
Text("Lorem Ipsum is simply dummy text of the printing and typesetting industry")
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
.clipped()
})
Spacer(minLength: 0)
/// Controls Views
VStack(spacing: 35) {
Button("", systemImage: reel.isLiked ? "suit.heart.fill":"suit.heart") {
self.reel.isLiked.toggle()
}
.symbolEffect(.bounce, value: reel.isLiked)
.foregroundStyle(reel.isLiked ? .red : .white)
Button("", systemImage: "message") { }
Button("", systemImage: "paperplane") { }
Button("", systemImage: "ellipsis") { }
}
.font(.title2)
.foregroundStyle(.white)
}
.padding(.leading, 15)
.padding(.trailing, 10)
.padding(.bottom, safeArea.bottom + 15)
}
}


#Preview(body: {
ContentView()
})

播放逻辑 算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// Play / Pause Action

func playPause(_ rect: CGRect) {

print("minY -----\(rect.minY)")

if **-rect.minY < (rect.height \* 0.5) && rect.minY < (rect.height \* 0.5)** {

player?.play()

} else {

player?.pause()

}

if rect.minY >= size.height || -rect.minY >= size.height {

player?.seek(to: .zero) // 重置到开始位置

}



}

ui.png

回顾页面结构

  • HomeView

    • Scrollview

      • Overlay ( 其他控件 不跟着 scrollView 滚动 )

          • 点赞双击的红心(带偏移动画)
      • LazyVStack (记住是 LazyVStack)

        • ReelView

        • ReelView

        • ReelView …

          • CustomVideoPlayer

            • Overlay ( 界面具体元素 [ 点赞 评论 私信 更多 ] )

            • onTapGesture(count: 2 …)

              • likedCounter数组 isLiked = true 触发点赞动画效果
            • onAppear

              • 新建 player [ player = queue ]
              • AVPlayerLooper ( 循环播放 )
              • 播放完成之后 player.seek(to: .zero) 重头开始
            • onDisappear

              • player = nil
      • overlay ( scrollView 界面上其他元素 [ Reels标题, )

        • 标题 Reels
        • Camera 图标📷

回顾 3 月 12 号

记不住的点

布局没什么问题

  • 数据共享绑定有点迷糊 @Binding
  • 通过OffsetKey来拿到scrollView 滚动的 size 变化控制播放▶️暂停 ⏸
  • 点击屏幕双击计算 添加一个 item 给 likeCounter; 动画效果给 likeCounter 动画添加一个counter.isLiked = true 动画完成之后 移除这个 id 的 likeCounter
  • 然后再 Home 页面的 scrollView 上面通过 Zstack 添加 forEach 的红心, 并给定frame , .animation , .offset(x: …, y: …), .offset(y:… )

首页滚动模式

1
2
3
4
5
6
7
 lazyVStack {
ReelView()
.frame(maxWidth: .infinity) // 在 ScrollView 下面就能通过子控件确定宽度
.containerRelativeFrame(.vertical) // 自定义 frame
}
}
.scrollTargetBehavior(.page) // 一页一个屏幕

以上就能确定基本翻页 Reel 的效果了.

scrollTargetBehavior.png

CATALOG
  1. 1. SwiftUI -ReelsLayout App
    1. 1.0.0.0.1. 效果图
    2. 1.0.0.0.2.
    3. 1.0.0.0.3. 类似于tiktok 播放页面, 滑动切换
  • 2. 页面结构
    1. 2.0.0.1. 按照页面page 样式, 竖屏滑动
  • 2.1. ReelVIew 代码
  • 2.2. 播放逻辑 算法
  • 2.3. 回顾页面结构
    1. 2.3.1. 回顾 3 月 12 号
      1. 2.3.1.1. 首页滚动模式