您好,我正在使用 YouTube Shorts 制作 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
}
}
更新代码
struct SingleVideoView: View {
let link: String
@State private var isVideoPlaying = false
@State private var destroy = false
@EnvironmentObject var viewModel: VideoModel
var body: some View {
ZStack {
SmartReelView(link: link, isPlaying: $isVideoPlaying, destroy: $destroy)
Color.gray.opacity(0.001)
.onTapGesture {
isVideoPlaying.toggle()
}
}
.onDisappear {
isVideoPlaying = false
}
.onAppear {
if viewModel.selected == link {
isVideoPlaying = true
destroy = false
}
}
.onChange(of: viewModel.selected, perform: { _ in
if viewModel.selected != link {
isVideoPlaying = false
if let x = viewModel.VideosToShow.firstIndex(where: { $0.videoID == viewModel.selected }), let j = viewModel.VideosToShow.firstIndex(where: { $0.videoID == link }){
if (x - j) > 2 && !destroy {
destroy = true
print("destroy \(j)")
}
}
}
})
}
}
struct SmartReelView: UIViewRepresentable {
let link: String
@Binding var isPlaying: Bool
@Binding var destroy: 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 createView(context: Context) { //copy of makeUIView but doesnt return a webview
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)
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if destroy && uiView.navigationDelegate != nil {
destroyWebView(uiView)
} else if uiView.navigationDelegate == nil {
createView(context: context)
}
//rest of code
}
private func destroyWebView(_ webView: WKWebView) {
print("destroyed")
webView.navigationDelegate = nil
webView.stopLoading()
webView.removeFromSuperview()
}
class Coordinator: NSObject, WKNavigationDelegate {
var parent: SmartReelView
init(_ parent: SmartReelView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
//rest of code
}
}
private func loadInitialContent(in webView: WKWebView) {
let embedHTML = """
//unchanged
"""
webView.scrollView.isScrollEnabled = false
webView.loadHTMLString(embedHTML, baseURL: nil)
}
}
为了优化,您可以考虑视图回收机制(“视图池”),以便重用 Web 视图实例,而不是每次显示新视频时创建新实例。
但是,由于您具体询问当用户超过 2 个视频时如何销毁 Web 视图,因此您可以实现逻辑来手动取消分配这些 Web 视图并清除其内容。
要手动销毁
WKWebView
,您需要:navigationDelegate
和设置UIDelegate
为nil
。stopLoading
它的方法。nil
(如果没有对 Web 视图留下强引用,这通常由 ARC 处理)。首先,添加一个标志
SmartReelView
来检查 Web 视图是否处于活动状态:更新
updateUIView
和makeUIView
方法以考虑活动状态:然后,在 中,引入逻辑以根据用户滚动的距离来
SingleVideoView
更新绑定。isActive
您可能需要传递一个索引并计算视图是否在当前活动视频的 2 个视频之内:在 中
AllVideoView
,维护当前活动视频索引的状态:将此索引传递给每个
SingleVideoView
:activeIndex
最后,每当 的选择发生变化时更新TabView
。这些更改应将内存中的 Web 视图数量限制为仅当前活动视频的 2 个视频内的 Web 视图数量,这应可缓解资源问题。
updateUIView
not being called consistently should be due to SwiftUI's optimization; it does not update the views that are not currently on the screen. Since you are working with aTabView
, SwiftUI tries to be efficient by not updating the invisible tabs.Directly holding a SwiftUI
View
is not recommended due to SwiftUI's declarative nature. However, you can maintain some state or object that both yourSingleVideoView
andSmartReelView
can refer to for coordinating the destruction and recreation of web views.For example, you could have a
VideoState
object that can be shared:You can then pass this shared object to both
SingleVideoView
andSmartReelView
.If a
WebView
has been destroyed (i.e., set to nil), SwiftUI will not automatically recreate it. You would have to manually trigger the recreation ofUIViewRepresentable
view.Try and add a
VideoState
object to yourSingleVideoView
.Update your
SmartReelView
to useVideoState
.Add logic to
SingleVideoView
to updateisActive
.And make sure to rebuild the
SmartReelView
ifwebView
is nil:That way, you coordinate between the
SingleVideoView
andSmartReelView
using the sharedVideoState
object. That allows you to destroy or rebuild web views based onisActive
state, and to also keep a reference to theWKWebView
if needed.I would still consider instead a pool of
WKWebView
instances, which involves maintaining a collection of reusable views, handing them out when needed, and returning them to the pool when they are no longer in use.A simplified example (focusing on the WebView pool) would include first a
WebViewPool
Manager.That manager will handle the logic for pooling:
You can then create an instance of this manager in your SwiftUI View where the web views are needed, for example in
AllVideoView
.And in the
SingleVideoView
orSmartReelView
, you can use the pool to get a web view when the view appears and return it when it disappears.That does not cover all edge cases. And managing the lifecycle (check out and return) of web views needs to be more nuanced. Depending on your needs, you might check out a web view not only when the view appears, but also when a new video needs to be loaded.
Still, the idea remains: by reusing the web views this way, you would minimize the overhead of creating and destroying web view instances, which should improve the performance of your application.