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

@ -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 = "<group>"; };
51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSelectionOperation.swift; sourceTree = "<group>"; };
51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSourceOperation.swift; sourceTree = "<group>"; };
51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.swift; sourceTree = "<group>"; };
51E36E70239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedSelectFolderTableViewCell.swift; sourceTree = "<group>"; };
51E36E8B239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddWebFeedSelectFolderTableViewCell.xib; sourceTree = "<group>"; };
51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
@ -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 */,

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