From 5a5abb0b8772702102df7f8ed96e4b6b2c19ea49 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 25 Feb 2020 15:10:51 -0800 Subject: [PATCH] Ensure that the dom is fully loaded on *all* web views before being made available to process JavaScript. Issue #1756 & Issue #1808 --- NetNewsWire.xcodeproj/project.pbxproj | 4 + iOS/Article/PreloadedWebView.swift | 81 +++++++++++++++++++ iOS/Article/WebViewController.swift | 8 +- iOS/Article/WebViewProvider.swift | 112 +++++--------------------- 4 files changed, 109 insertions(+), 96 deletions(-) create mode 100644 iOS/Article/PreloadedWebView.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 749281e4b..56562fec1 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -246,6 +246,7 @@ 51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; }; 51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */; }; 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */; }; + 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */; }; 51E36E71239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E36E70239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift */; }; 51E36E8C239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51E36E8B239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib */; }; 51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; }; @@ -1383,6 +1384,7 @@ 51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = ""; }; 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSelectionOperation.swift; sourceTree = ""; }; 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSourceOperation.swift; sourceTree = ""; }; + 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.swift; sourceTree = ""; }; 51E36E70239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedSelectFolderTableViewCell.swift; sourceTree = ""; }; 51E36E8B239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddWebFeedSelectFolderTableViewCell.xib; sourceTree = ""; }; 51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; @@ -2015,6 +2017,7 @@ 518651D9235621840078E021 /* ImageTransition.swift */, 5142192923522B5500E07E2C /* ImageViewController.swift */, 512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */, + 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */, 51AB8AB223B7F4C6008F147D /* WebViewController.swift */, 517630222336657E00E15FFF /* WebViewProvider.swift */, 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */, @@ -4009,6 +4012,7 @@ 51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */, 514219372352510100E07E2C /* ImageScrollView.swift in Sources */, 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */, + 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */, C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */, 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, diff --git a/iOS/Article/PreloadedWebView.swift b/iOS/Article/PreloadedWebView.swift new file mode 100644 index 000000000..1573b6a3d --- /dev/null +++ b/iOS/Article/PreloadedWebView.swift @@ -0,0 +1,81 @@ +// +// PreloadedWebView.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 2/25/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation +import WebKit + +class PreloadedWebView: WKWebView { + + private struct MessageName { + static let domContentLoaded = "domContentLoaded" + } + + private var isReady: Bool = false + private var readyCompletion: ((PreloadedWebView) -> Void)? + + init(articleIconSchemeHandler: ArticleIconSchemeHandler) { + let preferences = WKPreferences() + preferences.javaScriptCanOpenWindowsAutomatically = false + preferences.javaScriptEnabled = true + + let configuration = WKWebViewConfiguration() + configuration.preferences = preferences + configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs") + configuration.allowsInlineMediaPlayback = true + configuration.mediaTypesRequiringUserActionForPlayback = .video + configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) + + super.init(frame: .zero, configuration: configuration) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func preload() { + configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.domContentLoaded) + loadFileURL(ArticleRenderer.page.url, allowingReadAccessTo: ArticleRenderer.page.baseURL) + } + + func ready(completion: @escaping (PreloadedWebView) -> Void) { + if isReady { + completeRequest(completion: completion) + } else { + readyCompletion = completion + } + } + +} + +// MARK: WKScriptMessageHandler + +extension PreloadedWebView: WKScriptMessageHandler { + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + if message.name == MessageName.domContentLoaded { + isReady = true + if let completion = readyCompletion { + completeRequest(completion: completion) + readyCompletion = nil + } + } + } + +} + +// MARK: Private + +private extension PreloadedWebView { + + func completeRequest(completion: @escaping (PreloadedWebView) -> Void) { + isReady = false + configuration.userContentController.removeScriptMessageHandler(forName: MessageName.domContentLoaded) + completion(self) + } + +} diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 111a1e23f..528e863b8 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -29,8 +29,8 @@ class WebViewController: UIViewController { private var topShowBarsViewConstraint: NSLayoutConstraint! private var bottomShowBarsViewConstraint: NSLayoutConstraint! - private var webView: WKWebView? { - return view.subviews[0] as? WKWebView + private var webView: PreloadedWebView? { + return view.subviews[0] as? PreloadedWebView } private lazy var contextMenuInteraction = UIContextMenuInteraction(delegate: self) @@ -450,7 +450,7 @@ private extension WebViewController { } - func recycleWebView(_ webView: WKWebView?) { + func recycleWebView(_ webView: PreloadedWebView?) { guard let webView = webView else { return } webView.removeFromSuperview() @@ -467,7 +467,7 @@ private extension WebViewController { coordinator.webViewProvider.enqueueWebView(webView) } - func renderPage(_ webView: WKWebView?) { + func renderPage(_ webView: PreloadedWebView?) { guard let webView = webView else { return } let style = ArticleStylesManager.shared.currentStyle diff --git a/iOS/Article/WebViewProvider.swift b/iOS/Article/WebViewProvider.swift index 8fda2d206..e48b16764 100644 --- a/iOS/Article/WebViewProvider.swift +++ b/iOS/Article/WebViewProvider.swift @@ -11,21 +11,14 @@ import WebKit /// WKWebView has an awful behavior of a flash to white on first load when in dark mode. /// Keep a queue of WebViews where we've already done a trivial load so that by the time we need them in the UI, they're past the flash-to-shite part of their lifecycle. -class WebViewProvider: NSObject, WKNavigationDelegate { +class WebViewProvider: NSObject { - private struct MessageName { - static let domContentLoaded = "domContentLoaded" - } - let articleIconSchemeHandler: ArticleIconSchemeHandler private let minimumQueueDepth = 3 private let maximumQueueDepth = 6 private var queue = UIView() - private var waitingForFirstLoad = true - private var waitingCompletionHandler: ((WKWebView) -> ())? - init(coordinator: SceneCoordinator, viewController: UIViewController) { articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator) super.init() @@ -47,104 +40,39 @@ class WebViewProvider: NSObject, WKNavigationDelegate { func flushQueue() { queue.subviews.forEach { $0.removeFromSuperview() } - waitingForFirstLoad = true } func replenishQueueIfNeeded() { while queue.subviews.count < minimumQueueDepth { - let webView = WKWebView(frame: .zero, configuration: buildConfiguration()) - enqueueWebView(webView) + enqueueWebView(PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler)) } } - func dequeueWebView(completion: @escaping (WKWebView) -> ()) { - if waitingForFirstLoad { - waitingCompletionHandler = completion - } else { - completeRequest(completion: completion) + func dequeueWebView(completion: @escaping (PreloadedWebView) -> ()) { + if let webView = queue.subviews.last as? PreloadedWebView { + webView.ready { preloadedWebView in + preloadedWebView.removeFromSuperview() + self.replenishQueueIfNeeded() + completion(preloadedWebView) + } + return + } + + assertionFailure("Creating PreloadedWebView in \(#function); queue has run dry.") + + let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler) + webView.ready { preloadedWebView in + self.replenishQueueIfNeeded() + completion(preloadedWebView) } } - func enqueueWebView(_ webView: WKWebView) { + func enqueueWebView(_ webView: PreloadedWebView) { guard queue.subviews.count < maximumQueueDepth else { return } - - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.domContentLoaded) queue.insertSubview(webView, at: 0) - - webView.loadFileURL(ArticleRenderer.page.url, allowingReadAccessTo: ArticleRenderer.page.baseURL) - } - - // MARK: WKNavigationDelegate - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - if waitingForFirstLoad { - waitingForFirstLoad = false - if let completion = waitingCompletionHandler { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.completeRequest(completion: completion) - self.waitingCompletionHandler = nil - } - } - } + webView.preload() } } - -// MARK: WKScriptMessageHandler - -extension WebViewProvider: WKScriptMessageHandler { - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - switch message.name { - case MessageName.domContentLoaded: - if waitingForFirstLoad { - waitingForFirstLoad = false - if let completion = waitingCompletionHandler { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.completeRequest(completion: completion) - self.waitingCompletionHandler = nil - } - } - } - default: - return - } - } - -} - -// MARK: Private - -private extension WebViewProvider { - - func completeRequest(completion: @escaping (WKWebView) -> ()) { - if let webView = queue.subviews.last as? WKWebView { - webView.removeFromSuperview() - webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.domContentLoaded) - replenishQueueIfNeeded() - completion(webView) - return - } - - assertionFailure("Creating WKWebView in \(#function); queue has run dry.") - let webView = WKWebView(frame: .zero) - completion(webView) - } - - func buildConfiguration() -> WKWebViewConfiguration { - let preferences = WKPreferences() - preferences.javaScriptCanOpenWindowsAutomatically = false - preferences.javaScriptEnabled = true - - let configuration = WKWebViewConfiguration() - configuration.preferences = preferences - configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs") - configuration.allowsInlineMediaPlayback = true - configuration.mediaTypesRequiringUserActionForPlayback = .video - configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) - - return configuration - } -}