// // WebViewProvider.swift // 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. class WebViewProvider: NSObject, WKNavigationDelegate { 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() viewController.view.insertSubview(queue, at: 0) 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() { replenishQueueIfNeeded() } 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) } } func dequeueWebView(completion: @escaping (WKWebView) -> ()) { if waitingForFirstLoad { waitingCompletionHandler = completion } else { completeRequest(completion: completion) } } func enqueueWebView(_ webView: WKWebView) { 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 } } } } } // 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 } }