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 */; };
|
||||
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 */,
|
||||
|
|
|
@ -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 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
|
||||
|
|
|
@ -11,11 +11,7 @@ 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 {
|
||||
|
||||
private struct MessageName {
|
||||
static let domContentLoaded = "domContentLoaded"
|
||||
}
|
||||
class WebViewProvider: NSObject {
|
||||
|
||||
let articleIconSchemeHandler: ArticleIconSchemeHandler
|
||||
|
||||
|
@ -23,9 +19,6 @@ class WebViewProvider: NSObject, WKNavigationDelegate {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue