您好,我正在使用 YouTube 短片制作 TikTok 克隆。我在垂直选项卡视图中呈现视频,允许用户滚动视频列表。由于这些视频位于网络上,我使用网络视图来渲染它们。当用户滚动浏览选项卡视图时,将为新视频创建网络视图的新实例。当用户向后滚动时,他们可以在相同的持续时间内看到之前的视频(已渲染)。这意味着当用户从网页上滑开时,网页视图不会被破坏。滚动几分钟后,由于大量 Web 视图实例需要大量资源,设备会明显变热。当用户超过 2 个视频时,我如何销毁这些网络视图。
import SwiftUI
import WebKit
import UIKit
struct AllVideoView: View {
@State private var selected = ""
@State private var arr = ["-q6-DxWZnlQ", "Bp3iu47RRJQ", "lXJdgDjw1Ks", "It3ecCpuzgc", "7WNJjr8QM1w", "z2t0W8YSzZo", "w8RBGoH_6BM", "DJNAUBoxW5g", "Gv0X34FZ_8M", "EUTsaD1JFZE",
"yM9iLvOL2v4", "lnqhfn2n-Jo", "qkUpWwUAFPA", "Uz21KTMGwAI", "682rP7VrMUI",
"4AOcYT6tnsE", "DEz9ngMqVT0", "VOY2MviU5ig", "F8DvoxgP77M", "LGiRWOawMiw",
"Ub8j6l35VEM", "0xEQbJxR2hw", "SVow553Lluc", "0cPTM7v0vlw", "G12vO9ziK0k"]
var body: some View {
ZStack {
Color.black.edgesIgnoringSafeArea([.bottom, .top])
TabView(selection: $selected){
ForEach(arr, id: \.self){ id in
SingleVideoView(link: id).tag(id)
}
.rotationEffect(.init(degrees: -90))
.frame(width: widthOrHeight(width: true), height: widthOrHeight(width: false))
}
.offset(x: -10.5)
.frame(width: widthOrHeight(width: false), height: widthOrHeight(width: true))
.rotationEffect(.init(degrees: 90))
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
}
struct SingleVideoView: View {
let link: String
@State private var viewIsShowing = false
@State private var isVideoPlaying = false
var body: some View {
ZStack {
Color.black
SmartReelView(link: link, isPlaying: $isVideoPlaying, viewIsShowing: $viewIsShowing)
Button("", action: {}).disabled(true)
Color.gray.opacity(0.001)
.onTapGesture {
isVideoPlaying.toggle()
}
}
.ignoresSafeArea()
.onDisappear {
isVideoPlaying = false
viewIsShowing = false
}
.onAppear {
viewIsShowing = true
isVideoPlaying = true
}
}
}
struct SmartReelView: UIViewRepresentable {
let link: String
@Binding var isPlaying: Bool
@Binding var viewIsShowing: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let webConfiguration = WKWebViewConfiguration()
webConfiguration.allowsInlineMediaPlayback = true
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.navigationDelegate = context.coordinator
let userContentController = WKUserContentController()
webView.configuration.userContentController = userContentController
loadInitialContent(in: webView)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
var jsString = """
isPlaying = \((isPlaying) ? "true" : "false");
watchPlayingState();
"""
uiView.evaluateJavaScript(jsString, completionHandler: nil)
}
class Coordinator: NSObject, WKNavigationDelegate {
var parent: SmartReelView
init(_ parent: SmartReelView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if self.parent.viewIsShowing {
webView.evaluateJavaScript("clickReady()", completionHandler: nil)
}
}
}
private func loadInitialContent(in webView: WKWebView) {
let embedHTML = """
<style>
body {
margin: 0;
background-color: black;
}
.iframe-container iframe {
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<div class="iframe-container">
<div id="player"></div>
</div>
<script>
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
var player;
var isPlaying = false;
function onYouTubeIframeAPIReady() {
player = new YT.Player('player', {
width: '100%',
videoId: '\(link)',
playerVars: { 'playsinline': 1, 'controls': 0},
events: {
'onStateChange': function(event) {
if (event.data === YT.PlayerState.ENDED) {
player.seekTo(0);
player.playVideo();
}
}
}
});
}
function clickReady() {
player.playVideo();
}
function watchPlayingState() {
if (isPlaying) {
player.playVideo();
} else {
player.pauseVideo();
}
}
</script>
"""
webView.scrollView.isScrollEnabled = false
webView.loadHTMLString(embedHTML, baseURL: nil)
}
}
func widthOrHeight(width: Bool) -> CGFloat {
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
if width {
return window?.screen.bounds.width ?? 0
} else {
return window?.screen.bounds.height ?? 0
}
}
为了优化,您可以考虑视图回收机制(“视图池”),以便重用 Web 视图实例,而不是每次显示新视频时创建新实例。
但是,由于您专门询问当用户超过 2 个视频时如何销毁 Web 视图,因此您可以实现逻辑来手动取消分配这些 Web 视图并清除其内容。
要手动销毁
WKWebView
,您需要:
navigationDelegate
和 UIDelegate
设置为 nil
。stopLoading
方法。nil
(如果没有对 Web 视图留下强引用,这通常由 ARC 处理)。首先,在
SmartReelView
中添加一个标志来检查 Web 视图是否处于活动状态:
@Binding var isActive: Bool
更新
updateUIView
和 makeUIView
方法以考虑活动状态:
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
// existing code
if isActive {
loadInitialContent(in: webView)
}
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if isActive {
// existing code
} else {
destroyWebView(uiView)
}
}
private func destroyWebView(_ webView: WKWebView) {
webView.navigationDelegate = nil
webView.stopLoading()
webView.removeFromSuperview()
}
然后,在
SingleVideoView
中,引入逻辑以根据用户滚动的距离更新 isActive
绑定。您可能需要传递索引并计算视图是否在当前活动视频的 2 个视频之内:
struct SingleVideoView: View {
// existing properties
@Binding var activeIndex: Int // The index of the currently active (visible) video
let index: Int // The index of this particular video
var body: some View {
// existing code
SmartReelView(link: link, isPlaying: $isVideoPlaying, viewIsShowing: $viewIsShowing, isActive: .constant(shouldActivate))
}
private var shouldActivate: Bool {
return abs(activeIndex - index) <= 2
}
}
在
AllVideoView
中,维护当前活动视频索引的状态:
@State private var activeIndex = 0
将此索引传递给每个
SingleVideoView
:
ForEach(arr.indices, id: \.self) { index in
SingleVideoView(link: arr[index], activeIndex: $activeIndex, index: index)
}
最后,只要
activeIndex
的选择发生变化,就更新 TabView
。
这些更改应将内存中的网络视图数量限制为仅当前活动视频的 2 个视频内的网络视图数量,这应可缓解资源问题。
我仍然会考虑使用
WKWebView
实例池,这涉及维护可重用视图的集合,在需要时分发它们,并在不再使用时将它们返回到池中。
一个简化的示例(重点关注 WebView 池)将首先包含一个
WebViewPool
管理器。class WebViewPool {
private var pool: [WKWebView] = []
func getWebView() -> WKWebView {
if let webView = pool.first {
pool.removeFirst()
return webView
} else {
// Create a new web view, configure it as needed
let webView = WKWebView()
return webView
}
}
func returnWebView(_ webView: WKWebView) {
// Optionally clear the webView content
webView.loadHTMLString("", baseURL: nil)
pool.append(webView)
}
}
然后,您可以在需要 Web 视图的 SwiftUI 视图中创建此管理器的实例,例如在
AllVideoView
中。
struct AllVideoView: View {
@State private var webViewPool = WebViewPool()
//... existing code
}
并且在
SingleVideoView
或SmartReelView
中,您可以使用池在视图出现时获取Web视图,并在视图消失时返回它。
struct SingleVideoView: View {
let link: String
@Binding var webViewPool: WebViewPool
//... existing code
var body: some View {
// existing code
SmartReelView(link: link, webViewPool: $webViewPool)
.onAppear {
// Check out a WebView when appearing
}
.onDisappear {
// Return WebView when disappearing
}
}
}
struct SmartReelView: UIViewRepresentable {
let link: String
@Binding var webViewPool: WebViewPool
var webView: WKWebView?
func makeUIView(context: Context) -> WKWebView {
webView = webViewPool.getWebView()
// existing code
return webView!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
// existing code
}
func dismantleUIView(_ uiView: WKWebView, coordinator: Coordinator) {
webViewPool.returnWebView(uiView)
}
}
这并不能涵盖所有边缘情况。管理 Web 视图的生命周期(签出和返回)需要更加细致入微。根据您的需要,您不仅可以在视图出现时查看网络视图,还可以在需要加载新视频时查看。
但是,这个想法仍然存在:通过这种方式重用 Web 视图,您可以最大限度地减少创建和销毁 Web 视图实例的开销,这应该会提高应用程序的性能。