Olá, estou fazendo um clone do TikTok usando curtas do YouTube. Apresento vídeos em uma guia vertical que permite aos usuários rolar por uma lista de vídeos. Como esses vídeos estão na web, eu uso um webview para renderizá-los. À medida que o usuário rola pela guia, novas instâncias de visualizações da web são criadas para os novos vídeos. Quando o usuário rola para trás ele pode ver os vídeos anteriores (já renderizados) com a mesma duração. Isso significa que as visualizações da web não são destruídas quando o usuário sai delas. Depois de rolar por alguns minutos, o dispositivo fica visivelmente quente devido ao fato de que muitas instâncias de visualização da web exigem uma grande quantidade de recursos. Como posso destruir essas visualizações da web quando o usuário está a 2 vídeos de distância.
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
}
}
Código atualizado
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)
}
}
Para otimizar, você pode considerar um mecanismo de reciclagem de visualizações (um "pool de visualizações") para que as instâncias de visualização da Web sejam reutilizadas em vez de criar novas instâncias sempre que um novo vídeo for exibido.
No entanto, como você está perguntando especificamente como destruir visualizações da Web quando o usuário está a 2 vídeos de distância, você pode implementar uma lógica para desalocar manualmente essas visualizações da Web e limpar seu conteúdo.
Para destruir manualmente um
WKWebView
, você precisaria:navigationDelegate
eUIDelegate
comonil
.stopLoading
método nele.nil
(isso geralmente é tratado pelo ARC se não houver referências fortes à visualização da web).Primeiro, adicione um sinalizador
SmartReelView
para verificar se uma visualização da web está ativa:Atualize os métodos
updateUIView
emakeUIView
para considerar o estado ativo:Em seguida,
SingleVideoView
introduza a lógica para atualizar aisActive
ligação com base na distância que o usuário percorreu. Talvez seja necessário passar um índice e calcular se a visualização está a 2 vídeos do ativo no momento:Em
AllVideoView
, mantenha o estado do índice de vídeo atualmente ativo:Passe este índice para cada um
SingleVideoView
:Por fim, atualize
activeIndex
sempre que aTabView
seleção do for for alterada.Essas alterações devem limitar o número de visualizações da web na memória apenas àquelas que estão a 2 vídeos do atualmente ativo, o que deve mitigar o problema de recursos.
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.