Swift + Swift UI -修复iOS应用程序上TikTok克隆的错误

kninwzqo  于 2023-10-21  发布在  iOS
关注(0)|答案(1)|浏览(104)

我是iOS应用商店上一个社交媒体应用的所有者,我的平台包括一个类似TikTok的服务,可以显示YouTube短片。这个服务的用户界面是相当bug(bug包括下面),并不能正常工作。苹果要求我立即解决这些问题。如何改进此服务的功能?我在网页抓取和WebViews方面的知识非常有限。
下面是我想解决的问题:屏幕 Flink 白色,然后黑色的视频加载,我想有一个加载图标以上的视频封面显示,直到视频准备播放。

单视图

import SwiftUI
import WebKit
import UIKit

struct SingleVideoView: View {
    let link: String
    @State private var viewIsShowing = false
    @State private var isVideoPlaying = false
    @EnvironmentObject var viewModel: VideoModel

    var body: some View {
        ZStack {
            Color.black //so background always black
            
            SmartReelView(link: link, isPlaying: $isVideoPlaying, isChangingTime: $isChangingTime, totalLength: $totalLength, currentTime: $currentTime, viewIsShowing: $viewIsShowing)

            Button("", action: {}).disabled(true)       //disable any interaction with youtube video
            
            Color.gray.opacity(0.001)
                .onTapGesture {
                    isVideoPlaying.toggle()         //play pause video
                }
        }
        .ignoresSafeArea()
        .onDisappear {                  //stop playing when view leaves
            isVideoPlaying = false
            viewIsShowing = false
        }
        .onAppear {                     //try and play the video when view appears
            if viewModel.selected == link {
                viewIsShowing = true
                isVideoPlaying = true
            }
        }
        .onChange(of: viewModel.selected, perform: { _ in       //when the current video link changes play or pause
            if viewModel.selected == link {
                viewIsShowing = true
                isVideoPlaying = true
            } else if viewModel.selected != link {
                viewIsShowing = false
                isVideoPlaying = false
            }
        })
    }
}

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 userContentController = WKUserContentController()
        userContentController.add(context.coordinator, name: "observe")

        let webConfiguration = WKWebViewConfiguration()
        webConfiguration.allowsInlineMediaPlayback = true
        webConfiguration.userContentController = userContentController

        let webview = WKWebView(frame: .zero, configuration: webConfiguration)
        webview.navigationDelegate = context.coordinator

        loadInitialContent(web: 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, WKScriptMessageHandler  {
        var parent: SmartReelView

        init(_ parent: SmartReelView) {
            self.parent = parent
        }
        
        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage){
        }
        
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {     //try and force play the video
            if self.parent.viewIsShowing {
                webView.evaluateJavaScript("clickReady()", completionHandler: nil)
                
                //randomly try again incase took some time to load
                Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in
                    webView.evaluateJavaScript("clickReadySec()", completionHandler: nil)
                }
                Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
                    webView.evaluateJavaScript("clickReadySec()", completionHandler: nil)
                }
            }
        }
    }
    
    private func loadInitialContent(web: 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;
            var ready = 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) {      //restart video on end
                                player.seekTo(0);
                                player.playVideo();
                            }
                        }
                    }
                });
            }
            function clickReady() {
                if (ready) {
                    player.playVideo();
                }
            }
            function clickReadySec() {
                if (ready) {
                    var videoState = player.getPlayerState();
                    if (videoState !== YT.PlayerState.PLAYING) {
                        player.playVideo();
                    }
                }
            }
            function watchPlayingState() {
                if (isPlaying && ready) {
                    player.playVideo();
                } else {
                    player.pauseVideo();
                }
            }
        </script>
        """
        
        web.scrollView.isScrollEnabled = false
        web.loadHTMLString(embedHTML, baseURL: nil)
    }
}

标签视图包含单个视频

import SwiftUI

struct AllVideoView: View {
    @EnvironmentObject var viewModel: VideoModel
    
    var body: some View {
        ZStack {
            Color.black.edgesIgnoringSafeArea([.bottom, .top])
            TabView(selection: $viewModel.selected){
                ForEach(viewModel.VideosToShow){ video in
                    SingleVideoView(link: video).tag(video)
                }
                .rotationEffect(.init(degrees: -90))
                .frame(width: widthOrHeight(width: true), height: widthOrHeight(width: false))
            }
            .frame(width: widthOrHeight(width: false), height: widthOrHeight(width: true))
            .rotationEffect(.init(degrees: 90))
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
        }
    }
}

class VideoModel: ObservableObject {
    @Published var selected = ""
    
    //Data to test
    @Published var VideosToShow: [String] = ["iYebqpQ_EBA", "ZVAo11HS2lc", "j7f704_t6-U", "sZVW6dJk7_I", "8v7Tx2re5Rs", "xn90M86iUGw", "XTg3S-Ncr3I", "gJodMlgxyuY", "4Crbau26xdM", "wjSrqX_InUc", "4vaoZ0k7WC4", "wm-i3C2xi6E", "LOdHJKf8SaY", "T8SDP6rm-0k", "uIb5nrsWvxQ", "Qpbr5H34E_I", "MumEnIr_2vU", "6sTW7Q2uvo0", "ErnrcsM1CIk", "EuTUcRjtLbI", "xrovQjCSgtM", "DdlFT6ZPIR0", "HcpuMXsgMfI", "Xxk4mdojVTQ", "NJue1Wh4GZg", "xECgbg3SKg4", "pmqznJ1Pr64", "KKYplXrrOW0", "YuaeouCu8z4", "--Ay4om9zdQ", "950tFcmRtMc", "k56Ry6FLuYU", "QDpYHqDPYe0", "8JiB2ywr59c", "vAXR73OYU7c", "4tdS35_jUNc", "KYZeNx451Eo", "iEQQpdNbjdo", "KqwWLwKXUSA", "IDAmXhMj6Lo", "kd2g1xBs90g", "LFOIqZVv1Rc", "zCIeG6ht7jQ", "4co3RUJeebw", "Fb8YYJdgnmE", "ID1tfz4Hd1Q", "qSWzWYPOENY", "U-uIZLzgvfI", "R9r22u2gs_c", "3K0M69Npb1I", "DiHU8BJJ6jw", "4ZVsp4EkMb0", "SpHf4eBDjCk", "Rfudjg0jkvo", "EIFy41gYeVQ", "buQhVDlgbRg", "Rlz0vCw3bYs", "5Tv5xBn1AoI", "7Tv4RjbBSe8", "yhaFe_RzM14", "FDeMoHpyjJE", "h-RmBQ-GYY0", "e0rh5hgJJM0", "hjA4X_crpks", "wAVQ4u4V6vY", "NawgIgsyLTA", "OFtoXhXNEa0", "kPC9JW9c2Ro", "uhsy5vzmgls", "_Lm9ahDneqA", "u6Y6rbblFek", "U-C_XW8sZl4", "RHqGlX3lIEo", "yl0AOc45tQg", "C2V8K5yMBdo", "j8ROZ7-aIE8", "FODrGNH1SjA", "JYpWudy4jaQ", "QvqyeUaExNo", "WDYG2DwfJDk", "zz2Fxkf9u8M", "8bFp8CNyEus", "oLdPRJ8qI84", "dowoyaGmTgE", "GtLbiDqrs-0", "MyoH8blacsE", "7LS879KOdK0", "D4Qpeliwk4w", "nmMI3BM1ea8", "VGWgFary4Es", "9qqfxtOiTJ0", "Vt61xtJilHA", "URHFPuSNvt8", "pzX-PSaIV-c", "QaK8XcvvOEE", "pgk3d2VNbbo", "vKtZ5yqKY9A", "lVl-1ZTN8UY", "iKX79_6ZVpQ", "6FNjp772iyg", "ppaktgRUw4M", "qaf7uO5Hs0E", "3PfBSRfDa-8", "OGGLLoSyBCw", "_GXpYnTjjCY", "TDACktZbs9s", "qRzix60msYs", "UUBKHJB5xm0", "tuFsPpkyjes", "woSAr1TY-rs", "3mCkBvpIuYc", "O2YoEcY51SY", "af3dkkGqIH4", "Clo8frpXQRk", "JrCaYLvpTTo", "ag8MAvd6t94", "VOJVNyUtjUI", "ye5rxzDS_Bo", "SAZtnEuCnj0", "gO7bDxe-tWI", "iINSdcdhjBQ", "ysNIsT-1emU", "y97KR2XjMHA", "PkEeowOtqgE", "Z5NsgZQjjoc", "CeoIIi6oJiU", "-yMmZT0XyBg", "B7Uz78okItk", "IZieI2VnpXM", "Vsb4yho_hTk", "Lu4Ie_lJsKg", "NBgOY-kHWVQ", "YnFI3IS8duA", "3fIWRHQYRb8", "prLyItjvb0w", "-gSbmK1eN_Q", "3Zkfq9JYaf4", "2CvCIFPdgMo", "hUsDOf8s61o", "ha2_x5eJ1OY", "sqJ5fEqmh1w", "Q8nUj8EH8qM", "TXLA-qtWlj4", "uFu8YxtoZaU", "JTQ-zkZ9WQQ", "bYuLW07tGmI", "bCPuLceW-yE", "ot0d3ZgPrN8", "m0myvDUD5Js", "HJQ4TiCFvUY", "TS6xiAV1qHQ", "-38yIw4IhAY", "x-n0FwD8HFs", "uwbZ4UWbuhQ", "EhuAMPjuEks", "Nk5HgYdTRdg", "ztLTqCjrXrc", "q0VUZ2hAIuw", "S_vOu6oqCDE", "sh0hHSq5Ogc", "oyswMIUFr4w", "zoSjLJqJPDQ", "BLTnIBQJlFc", "pUYDN-stzck", "g1ucPFztCrc", "u4Pav23IzS4", "7eaAN0X_evA", "GIRrfccZvLI", "fVP7IansDAI", "41QxPIfWFN8", "7kFhNLEI7yY", "NH2OI2Q0q14", "as2WgzniPVo", "rjiteE2rKFs", "yz2TumE1ws0", "d-XwLHM8sFE", "uN4h4OmtfEk", "u2-7hhlMb6w", "-JaSHN3aIyY", "pZTjhMxas_s", "zN8fSWxHxVc", "D_uXLM3VJYs", "nk0hOKS_zfA", "EaImidqT0xQ", "7PlSHvyqh70", "epbrJg-LTOE", "9gE7MOnmHfs", "F5m2Qg2cst4", "kikGvjKFN8Q", "Kbvk8T6QXBE", "k-IXHTPWV8M", "uGiclr_zW2M", "rGIL7Yal2WA", "7-_LCq2UTA8", "VRvBcnQ7Yyo", "5R9zmI5Mbrc", "HaUm23dTR5g", "otUIqpojXqA", "5Jq0IlNzvM4", "21EofWX0uuo", "PacSMXd1FIc", "sEWZ7TPXXDM", "zbElpS_OiGc", "0jSmuCKvj7U", "uoiQChfa-c8", "jv1IKf_NQ50", "j4Yl886jRSU", "E6JSDl70zEY", "-fyklDM48ig", "7amjvAwvXes", "agwGF1FeWOQ", "tTft0J__BrA", "n_rf3Jjclrw", "gJDKloPhiMk", "vK3MG9yhn4o", "-UGDDKQE2FI", "a5RNe4vITws", "gf1Bvx6oVKw", "DCwSyZGQbEQ", "7G6ipKZfeLM", "UftB2CCWyHs", "Uf2FAF1rSJQ", "YC9hLAinWig", "g-5wvzYR8MY", "UsUoqDLJOnA", "BtkwBEts0Jg", "cKDbx1Udj_Q", "ax7kwHO4fvc", "xMYdyz5JteE", "VZjj1D3DGrw", "fW7dfJ7_PkE", "sEFUk8sb6vk", "4sTxfxxEZW0", "l2k1vzNno3A", "ZYe0k_dk5D4", "IBMshfQ-jCo", "pYJjIzj-t4c", "vK5riR3lbIM", "TiBAmEW03mM", "pLBPbJcJ_F4", "PQLJhhpXIDs", "ZRSjBQ_qIhE", "V9zTAsfN9Bw", "4dqiSzf-y-I", "L16S2yCRo10", "HctLr25JFx4", "l5WyvXvYRzc", "sbYofIDT4Y0", "3WEPgIitq6Y", "GBjoifXMkEc", "D7cdGKevHYk", "wU0oqTncwSc", "uCIYARYeaF8", "fzChmE4wzME", "dNmFhyMzckg", "bbvQBEtAJrU", "GMC57RwqPDg", "CRueXlxg24s", "2lpcP8LyZrs", "Y8n5LmBz8Vk", "WkfEO9aoNuc", "Vi1jtwUc0kU", "irVXM6JDUQg", "o1XPcaokECE", "5bMPf_E1-Vw", "RYMXdvLOIls", "Ksb5gWSJPQ8", "hEP5XqEu7P4", "rf5qRQugqgY", "ndzHTk1tJtk", "0MMQSjoLIVc", "_-YsXJ72eeo", "zBP4TsOWbH0", "LecyAUI93c0", "C0OA0WcN0BY", "DAKxPMg6oPw", "UeSum0pCm_U", "L8GxXFW_y6Q", "Nm0WQ26euoc", "dwEIVP6kKLY", "kd2g1xBs90g", "rlYBLoYUiPQ", "2BqPHZWT86Q", "T9ZuTZRxgag", "epRrztb3VHc", "_V0oZOOHvOw", "rLlbIz-UsW0", "U_eoUo7tEAE", "LHpUQdQDtyQ", "GGbzmEoTPWA", "nxmKRgO_a44", "iuB4OwPTa7M", "MI8nv441Q78", "pkETcBX15Ns", "A9xx98UqDd8", "2LTR5SstIhA", "OdAPkzyetZY", "B_SMTxXIAT4", "516ESehbjcw", "GqCBB_9drtg", "1mwybHblSoU", "AwPcbIGG9DQ", "9C7Rjy3GgqU", "AquwGe-LRNI", "l3QDF8KfMlQ", "oxWowAGygq4", "qkkptG9UXg4", "fudFlhI43JQ", "6IoJ3iaT_2Q", "hTNADe1fGIs", "L6z3TZaTKl8", "dlXqMvzye24", "8iXzfysURSc", "ukBKppVJYLo", "sPkkWBckt44", "ecB8x_pP-8M", "xjMAlSs5Ayg", "tna_mHHtChw", "https://youtube.com/shorts/xyhY92K0RDI?si=DZAYC6285acLMJud", "ASEZtFsbjVE", "HFpHOoPa3yo", "DJvQzI9bae4", "-uUP5YUBJ-0", "0sx9QHubgqc", "7OeKeFisbgg", "6Kp30WYyF5M", "K709BAWgqs4", "H5vLWyWMBgA", "iDgetgvbk7E", "xwakzmjkNe0", "ddjQMQyOqJU", "IThk6vqrVs8", "0cKQsIUOXx4", "CkCpplHbDzs", "dOQsXA30RWU", "01P0hQQgq-c", "Mt4hOuodhvo", "MSG_J4nZoA0", "TndspK834ms", "ivLRbslmuzE", "33fsguWtdXo", "ulQHG4CtIcc", "Jp9zpUtafxA", "HWy4w8QWrJc", "uvRWJr-WUTs", "MhlOPtLxBM0", "qvBaaK8voyw", "EW7FTWV1H0c", "X220GWe_lmQ", "3oy2hklCdZM", "Yzs1NecYPPw", "PTWTkfzQI5o", "0zEyTOb7Tr0", "KbzlC01D83c", "FQNP-gbyjZk", "JMWuo1XylM4", "t9_Nz91jxmY", "iYtdYc_WLM0", "Ak1lmnMzf-Y", "9sPRNYitLLY", "KpjTXmm7pGE", "K9Ym44tJbQE", "lg_VwVnpqIE", "kv9PCqLj3vo", "aSd1IQNm4_s", "gjCz0BBGtQE", "2Rl9TivKS34", "HArdqJHadtE", "Xs0mbQUy9qg", "5ksMWUPhG3M", "JnDR5lQMYb4", "Iwck0KLLTAw", "-Z9d9-4I21s", "f6W8DsmIC5k", "ibRhoFfbgZI", "KoW3Qr_Uttk", "HFgadwtV2U0", "tPgSLUBxwlo", "hJtrIFhHoiE", "q0_53pX1V40", "sQ64plrMdjY", "UIeOjGR1ols", "y2PAOVxHyzw", "ENfQ5Q9hzd4", "S81byfkCbzc", "TBNfnC8lj2c", "2X-pqstQ9Mg", "BlEWAH17wus", "DLKn3np-QXA", "M1jGiFhPyvc", "uaxvB2YcccA", "aJIA0FM-ko4", "1CZgcxiSS4w", "HjFoOMpMQ6c", "IpLvmKbWDEQ", "220CEbaowoA", "CK2cCr4At1o", "cjA2TVjx6Xg", "wY8IjPPWqbg", "5o7Fe1P078s", "VwQtD-ZVbzs", "CUPvgecdsck"]
}
iszxjhcz

iszxjhcz1#

视频嵌入在WKWebView中,相关的UI组件使用SwiftUI构建。但屏幕 Flink 白色,然后黑色的视频加载,和所需的结果是有视频封面显示与加载图标,直到视频准备播放。
为此,您可以考虑使用视频封面加载指示器

  • 要显示加载指示器和视频封面,您可以引入一个新的SwiftUI View,它覆盖在您的SingleVideoView之上。该覆盖视图可以包含加载指示符和用于显示视频封面的图像视图。
  • 利用ZStack将叠加视图堆叠在SmartReelView之上。
  • 一旦视频准备好播放,隐藏覆盖视图。这可以通过检查isPlaying状态或引入新状态来专门处理加载状态来实现。
struct LoadingOverlayView: View {
    var body: some View {
        VStack {
            ActivityIndicator(isAnimating: .constant(true), style: .large)
            ImageView()  // Assume this view displays your video cover
        }
    }
}
var body: some View {
    ZStack {
        Color.black //so background always black
        
        SmartReelView(link: link, isPlaying: $isVideoPlaying, isChangingTime: $isChangingTime, totalLength: $totalLength, currentTime: $currentTime, viewIsShowing: $viewIsShowing)
        
        if !isVideoPlaying {
            LoadingOverlayView()
        }
        
        Button("", action: {}).disabled(true)  //disable any interaction with youtube video
        
        Color.gray.opacity(0.001)
            .onTapGesture {
                isVideoPlaying.toggle()  //play pause video
            }
    }
    // rest of your code
}

SmartReelView中的JavaScript代码中,您可以考虑引入一种机制,在视频准备好播放时通知Swift代码。这可以使用YouTube Iframe Player API的onReady事件来完成。
使用WKScriptMessageHandler协议接收来自JavaScript代码的消息,并相应地更新SwiftUI视图。

function onYouTubeIframeAPIReady() {
    player = new YT.Player('player', {
        width: '100%',
        videoId: '\(link)',
        playerVars: { 'playsinline': 1, 'controls': 0},
        events: {
            'onReady': function(event) {
                // Notify your Swift code that the video is ready
                window.webkit.messageHandlers.observe.postMessage("ready");
            },
            'onStateChange': function(event) {
                if (event.data === YT.PlayerState.ENDED) {      //restart video on end
                    player.seekTo(0);
                    player.playVideo();
                }
            }
        }
    });
}
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler  {
    var parent: SmartReelView

    init(_ parent: SmartReelView) {
        self.parent = parent
    }
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "observe" && message.body as! String == "ready" {
            // Update your SwiftUI view to hide the loading overlay
            DispatchQueue.main.async {
                self.parent.isPlaying = true
            }
        }
    }
    
    // rest of your code
}

这将创建一个新的叠加视图来处理加载指示器和视频封面,沿着调整JavaScript和Swift交互,以便在视频准备播放时隐藏叠加。确保视频封面图像可用并在ImageView中正确显示至关重要。

相关问题