Ensure that the dom is fully loaded on *all* web views before being made available to process JavaScript. Issue #1756 & Issue #1808

This commit is contained in:
Maurice Parker
2020-02-25 15:10:51 -08:00
parent a4bbf65944
commit 5a5abb0b87
4 changed files with 109 additions and 96 deletions

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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
}
}