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:
parent
a4bbf65944
commit
5a5abb0b87
|
@ -246,6 +246,7 @@
|
||||||
51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; };
|
51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; };
|
||||||
51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */; };
|
51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */; };
|
||||||
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.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 */; };
|
51E36E71239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E36E70239D6610006F47A5 /* AddWebFeedSelectFolderTableViewCell.swift */; };
|
||||||
51E36E8C239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51E36E8B239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib */; };
|
51E36E8C239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51E36E8B239D6765006F47A5 /* AddWebFeedSelectFolderTableViewCell.xib */; };
|
||||||
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2015,6 +2017,7 @@
|
||||||
518651D9235621840078E021 /* ImageTransition.swift */,
|
518651D9235621840078E021 /* ImageTransition.swift */,
|
||||||
5142192923522B5500E07E2C /* ImageViewController.swift */,
|
5142192923522B5500E07E2C /* ImageViewController.swift */,
|
||||||
512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */,
|
512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */,
|
||||||
|
51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */,
|
||||||
51AB8AB223B7F4C6008F147D /* WebViewController.swift */,
|
51AB8AB223B7F4C6008F147D /* WebViewController.swift */,
|
||||||
517630222336657E00E15FFF /* WebViewProvider.swift */,
|
517630222336657E00E15FFF /* WebViewProvider.swift */,
|
||||||
51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */,
|
51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */,
|
||||||
|
@ -4009,6 +4012,7 @@
|
||||||
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */,
|
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */,
|
||||||
514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
|
514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
|
||||||
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
|
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
|
||||||
|
51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */,
|
||||||
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
|
C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */,
|
||||||
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */,
|
51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */,
|
||||||
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
|
51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -29,8 +29,8 @@ class WebViewController: UIViewController {
|
||||||
private var topShowBarsViewConstraint: NSLayoutConstraint!
|
private var topShowBarsViewConstraint: NSLayoutConstraint!
|
||||||
private var bottomShowBarsViewConstraint: NSLayoutConstraint!
|
private var bottomShowBarsViewConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
private var webView: WKWebView? {
|
private var webView: PreloadedWebView? {
|
||||||
return view.subviews[0] as? WKWebView
|
return view.subviews[0] as? PreloadedWebView
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var contextMenuInteraction = UIContextMenuInteraction(delegate: self)
|
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 }
|
guard let webView = webView else { return }
|
||||||
|
|
||||||
webView.removeFromSuperview()
|
webView.removeFromSuperview()
|
||||||
|
@ -467,7 +467,7 @@ private extension WebViewController {
|
||||||
coordinator.webViewProvider.enqueueWebView(webView)
|
coordinator.webViewProvider.enqueueWebView(webView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderPage(_ webView: WKWebView?) {
|
func renderPage(_ webView: PreloadedWebView?) {
|
||||||
guard let webView = webView else { return }
|
guard let webView = webView else { return }
|
||||||
|
|
||||||
let style = ArticleStylesManager.shared.currentStyle
|
let style = ArticleStylesManager.shared.currentStyle
|
||||||
|
|
|
@ -11,21 +11,14 @@ import WebKit
|
||||||
|
|
||||||
/// WKWebView has an awful behavior of a flash to white on first load when in dark mode.
|
/// 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.
|
/// 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
|
let articleIconSchemeHandler: ArticleIconSchemeHandler
|
||||||
|
|
||||||
private let minimumQueueDepth = 3
|
private let minimumQueueDepth = 3
|
||||||
private let maximumQueueDepth = 6
|
private let maximumQueueDepth = 6
|
||||||
private var queue = UIView()
|
private var queue = UIView()
|
||||||
|
|
||||||
private var waitingForFirstLoad = true
|
|
||||||
private var waitingCompletionHandler: ((WKWebView) -> ())?
|
|
||||||
|
|
||||||
init(coordinator: SceneCoordinator, viewController: UIViewController) {
|
init(coordinator: SceneCoordinator, viewController: UIViewController) {
|
||||||
articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator)
|
articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator)
|
||||||
super.init()
|
super.init()
|
||||||
|
@ -47,104 +40,39 @@ class WebViewProvider: NSObject, WKNavigationDelegate {
|
||||||
|
|
||||||
func flushQueue() {
|
func flushQueue() {
|
||||||
queue.subviews.forEach { $0.removeFromSuperview() }
|
queue.subviews.forEach { $0.removeFromSuperview() }
|
||||||
waitingForFirstLoad = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func replenishQueueIfNeeded() {
|
func replenishQueueIfNeeded() {
|
||||||
while queue.subviews.count < minimumQueueDepth {
|
while queue.subviews.count < minimumQueueDepth {
|
||||||
let webView = WKWebView(frame: .zero, configuration: buildConfiguration())
|
enqueueWebView(PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler))
|
||||||
enqueueWebView(webView)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dequeueWebView(completion: @escaping (WKWebView) -> ()) {
|
func dequeueWebView(completion: @escaping (PreloadedWebView) -> ()) {
|
||||||
if waitingForFirstLoad {
|
if let webView = queue.subviews.last as? PreloadedWebView {
|
||||||
waitingCompletionHandler = completion
|
webView.ready { preloadedWebView in
|
||||||
} else {
|
preloadedWebView.removeFromSuperview()
|
||||||
completeRequest(completion: completion)
|
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 {
|
guard queue.subviews.count < maximumQueueDepth else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.domContentLoaded)
|
|
||||||
queue.insertSubview(webView, at: 0)
|
queue.insertSubview(webView, at: 0)
|
||||||
|
webView.preload()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue