2019-09-21 17:37:21 +02:00
|
|
|
//
|
2020-01-01 00:55:39 +01:00
|
|
|
// WebViewProvider.swift
|
2019-09-21 17:37:21 +02:00
|
|
|
// NetNewsWire-iOS
|
|
|
|
//
|
|
|
|
// Created by Maurice Parker on 9/21/19.
|
|
|
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
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.
|
2020-01-01 00:55:39 +01:00
|
|
|
class WebViewProvider: NSObject, WKNavigationDelegate {
|
2019-09-21 17:37:21 +02:00
|
|
|
|
2020-02-05 01:00:26 +01:00
|
|
|
private struct MessageName {
|
|
|
|
static let domContentLoaded = "domContentLoaded"
|
|
|
|
}
|
|
|
|
|
2020-01-27 20:58:32 +01:00
|
|
|
let articleIconSchemeHandler: ArticleIconSchemeHandler
|
2019-11-07 21:29:16 +01:00
|
|
|
|
2019-09-21 17:37:21 +02:00
|
|
|
private let minimumQueueDepth = 3
|
|
|
|
private let maximumQueueDepth = 6
|
2020-01-30 01:39:40 +01:00
|
|
|
private var queue = UIView()
|
2019-09-21 17:37:21 +02:00
|
|
|
|
|
|
|
private var waitingForFirstLoad = true
|
|
|
|
private var waitingCompletionHandler: ((WKWebView) -> ())?
|
|
|
|
|
2020-01-30 01:39:40 +01:00
|
|
|
init(coordinator: SceneCoordinator, viewController: UIViewController) {
|
2020-01-27 20:58:32 +01:00
|
|
|
articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator)
|
|
|
|
super.init()
|
2020-01-30 20:53:27 +01:00
|
|
|
viewController.view.insertSubview(queue, at: 0)
|
2020-02-05 01:00:26 +01:00
|
|
|
|
|
|
|
replenishQueueIfNeeded()
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func didEnterBackground() {
|
|
|
|
flushQueue()
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func willEnterForeground() {
|
2020-01-27 20:58:32 +01:00
|
|
|
replenishQueueIfNeeded()
|
|
|
|
}
|
2020-02-05 01:00:26 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2020-01-27 20:58:32 +01:00
|
|
|
|
2019-09-21 17:37:21 +02:00
|
|
|
func dequeueWebView(completion: @escaping (WKWebView) -> ()) {
|
|
|
|
if waitingForFirstLoad {
|
|
|
|
waitingCompletionHandler = completion
|
|
|
|
} else {
|
|
|
|
completeRequest(completion: completion)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func enqueueWebView(_ webView: WKWebView) {
|
2020-01-30 01:39:40 +01:00
|
|
|
guard queue.subviews.count < maximumQueueDepth else {
|
2019-09-21 17:37:21 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-02-05 01:00:26 +01:00
|
|
|
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.domContentLoaded)
|
2020-01-30 01:39:40 +01:00
|
|
|
queue.insertSubview(webView, at: 0)
|
2019-09-21 17:37:21 +02:00
|
|
|
|
2020-01-27 04:29:58 +01:00
|
|
|
webView.loadFileURL(ArticleRenderer.page.url, allowingReadAccessTo: ArticleRenderer.page.baseURL)
|
2019-09-21 17:37:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: WKNavigationDelegate
|
|
|
|
|
|
|
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
|
if waitingForFirstLoad {
|
|
|
|
waitingForFirstLoad = false
|
|
|
|
if let completion = waitingCompletionHandler {
|
2020-02-02 01:41:10 +01:00
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
|
|
self.completeRequest(completion: completion)
|
|
|
|
self.waitingCompletionHandler = nil
|
|
|
|
}
|
2019-09-21 17:37:21 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-05 01:00:26 +01:00
|
|
|
}
|
2019-09-21 17:37:21 +02:00
|
|
|
|
2020-02-05 01:00:26 +01:00
|
|
|
// 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
|
2019-09-21 17:37:21 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-05 01:00:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Private
|
|
|
|
|
|
|
|
private extension WebViewProvider {
|
|
|
|
|
|
|
|
func completeRequest(completion: @escaping (WKWebView) -> ()) {
|
2020-01-30 01:39:40 +01:00
|
|
|
if let webView = queue.subviews.last as? WKWebView {
|
|
|
|
webView.removeFromSuperview()
|
2020-02-05 01:00:26 +01:00
|
|
|
webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.domContentLoaded)
|
2019-09-21 17:37:21 +02:00
|
|
|
replenishQueueIfNeeded()
|
|
|
|
completion(webView)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
assertionFailure("Creating WKWebView in \(#function); queue has run dry.")
|
|
|
|
let webView = WKWebView(frame: .zero)
|
|
|
|
completion(webView)
|
|
|
|
}
|
2020-02-05 01:00:26 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2019-09-21 17:37:21 +02:00
|
|
|
}
|