diff --git a/iOS/AppCoordinator.swift b/iOS/AppCoordinator.swift index 1eaf7f4e5..415048466 100644 --- a/iOS/AppCoordinator.swift +++ b/iOS/AppCoordinator.swift @@ -237,6 +237,9 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) + + // Force lazy initialization of the web view provider so that it can warm up the queue of prepared web views + let _ = DetailViewControllerWebViewProvider.shared } func start() -> UIViewController { diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index 4c40e5500..63ea87669 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -15,22 +15,13 @@ - + - - - - - - + + + - - - - - - @@ -112,7 +103,7 @@ - + diff --git a/iOS/Detail/DetailViewController.swift b/iOS/Detail/DetailViewController.swift index 2eb994207..214e3dc8e 100644 --- a/iOS/Detail/DetailViewController.swift +++ b/iOS/Detail/DetailViewController.swift @@ -14,23 +14,42 @@ import SafariServices class DetailViewController: UIViewController { - @IBOutlet weak var nextUnreadBarButtonItem: UIBarButtonItem! - @IBOutlet weak var prevArticleBarButtonItem: UIBarButtonItem! - @IBOutlet weak var nextArticleBarButtonItem: UIBarButtonItem! - @IBOutlet weak var readBarButtonItem: UIBarButtonItem! - @IBOutlet weak var starBarButtonItem: UIBarButtonItem! - @IBOutlet weak var actionBarButtonItem: UIBarButtonItem! - @IBOutlet weak var browserBarButtonItem: UIBarButtonItem! - @IBOutlet weak var webView: WKWebView! - + @IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem! + @IBOutlet private weak var prevArticleBarButtonItem: UIBarButtonItem! + @IBOutlet private weak var nextArticleBarButtonItem: UIBarButtonItem! + @IBOutlet private weak var readBarButtonItem: UIBarButtonItem! + @IBOutlet private weak var starBarButtonItem: UIBarButtonItem! + @IBOutlet private weak var actionBarButtonItem: UIBarButtonItem! + @IBOutlet private weak var browserBarButtonItem: UIBarButtonItem! + @IBOutlet private weak var webViewContainer: UIView! + private var webView: WKWebView! + weak var coordinator: AppCoordinator! + deinit { + webView.removeFromSuperview() + DetailViewControllerWebViewProvider.shared.enqueueWebView(webView) + webView = nil + } + override func viewDidLoad() { - super.viewDidLoad() - webView.isHidden = true + + webView = DetailViewControllerWebViewProvider.shared.dequeueWebView() + webView.translatesAutoresizingMaskIntoConstraints = false webView.navigationDelegate = self + webViewContainer.addSubview(webView) + + let constraints: [NSLayoutConstraint] = [ + webView.leadingAnchor.constraint(equalTo: webViewContainer.safeAreaLayoutGuide.leadingAnchor), + webView.trailingAnchor.constraint(equalTo: webViewContainer.safeAreaLayoutGuide.trailingAnchor), + webView.topAnchor.constraint(equalTo: webViewContainer.safeAreaLayoutGuide.topAnchor), + webView.bottomAnchor.constraint(equalTo: webViewContainer.safeAreaLayoutGuide.bottomAnchor), + ] + + NSLayoutConstraint.activate(constraints) + markAsRead() updateUI() reloadHTML() @@ -213,16 +232,6 @@ extension DetailViewController: WKNavigationDelegate { } } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - // We initially hide the webview and only show it after it has loaded to avoid the - // white flashing that WKWebView does when it loads. This is especially noticable - // in dark mode. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - webView.isHidden = false - } - } - } private extension DetailViewController { @@ -234,3 +243,48 @@ private extension DetailViewController { } } + +// MARK: - + +/// 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 DetailViewControllerWebViewProvider { + static var shared = DetailViewControllerWebViewProvider() + + func dequeueWebView() -> WKWebView { + if let webView = queue.popLast() { + replenishQueueIfNeeded() + return webView + } + + assertionFailure("Creating WKWebView in \(#function); queue has run dry.") + let webView = WKWebView(frame: .zero) + return webView + } + + func enqueueWebView(_ webView: WKWebView) { + webView.uiDelegate = nil + webView.navigationDelegate = nil + + let html = ArticleRenderer.noSelectionHTML(style: .defaultStyle) + webView.loadHTMLString(html, baseURL: nil) + + queue.insert(webView, at: 0) + } + + // MARK: Private + + private let minimumQueueDepth = 3 + private var queue: [WKWebView] = [] + + private init() { + replenishQueueIfNeeded() + } + + private func replenishQueueIfNeeded() { + while queue.count < minimumQueueDepth { + let webView = WKWebView(frame: .zero) + enqueueWebView(webView) + } + } +}