diff --git a/Mac/MainWindow/Detail/DetailViewController.swift b/Mac/MainWindow/Detail/DetailViewController.swift
index 592322b0d..0e3def9e9 100644
--- a/Mac/MainWindow/Detail/DetailViewController.swift
+++ b/Mac/MainWindow/Detail/DetailViewController.swift
@@ -15,6 +15,7 @@ import RSWeb
enum DetailState: Equatable {
case noSelection
case multipleSelection
+ case loading
case article(Article)
case extracted(Article, ExtractedArticle)
}
diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift
index 43d9ed835..cdcd744fd 100644
--- a/Mac/MainWindow/Detail/DetailWebViewController.swift
+++ b/Mac/MainWindow/Detail/DetailWebViewController.swift
@@ -104,13 +104,8 @@ final class DetailWebViewController: NSViewController, WKUIDelegate {
NotificationCenter.default.addObserver(self, selector: #selector(webInspectorEnabledDidChange(_:)), name: .WebInspectorEnabledDidChange, object: nil)
#endif
- webView.loadHTMLString(template(), baseURL: nil)
- }
-
- func template() -> String {
- let path = Bundle.main.path(forResource: "page", ofType: "html")!
- let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
- return s as String
+ webView.loadHTMLString(ArticleRenderer.page.html, baseURL: ArticleRenderer.page.baseURL)
+
}
// MARK: Scrolling
@@ -182,13 +177,15 @@ private extension DetailWebViewController {
func reloadHTML() {
let style = ArticleStylesManager.shared.currentStyle
- let rendering: ArticleRendering
+ let rendering: ArticleRenderer.Rendering
switch state {
case .noSelection:
rendering = ArticleRenderer.noSelectionHTML(style: style)
case .multipleSelection:
rendering = ArticleRenderer.multipleSelectionHTML(style: style)
+ case .loading:
+ rendering = ArticleRenderer.loadingHTML(style: style)
case .article(let article):
rendering = ArticleRenderer.articleHTML(article: article, style: style)
case .extracted(let article, let extractedArticle):
diff --git a/Mac/MainWindow/Detail/page.html b/Mac/MainWindow/Detail/page.html
index 8178a7548..97e7c4541 100644
--- a/Mac/MainWindow/Detail/page.html
+++ b/Mac/MainWindow/Detail/page.html
@@ -2,33 +2,7 @@
-
+
diff --git a/Mac/MainWindow/Detail/styleSheet.css b/Mac/MainWindow/Detail/styleSheet.css
index 97ab0e019..107a7cb7e 100644
--- a/Mac/MainWindow/Detail/styleSheet.css
+++ b/Mac/MainWindow/Detail/styleSheet.css
@@ -133,6 +133,20 @@ figcaption {
line-height: 1.3em;
}
+.iframeWrap {
+ position: relative;
+ display: block;
+ padding-top: 56.25%;
+}
+
+.iframeWrap iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100% !important;
+ width: 100% !important;
+}
+
/*Block ads and junk*/
iframe[src*="feedads"],
diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift
index 21cf866ff..f863b0160 100644
--- a/Mac/MainWindow/MainWindowController.swift
+++ b/Mac/MainWindow/MainWindowController.swift
@@ -329,11 +329,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
detailViewController?.setState(detailState, mode: timelineSourceMode)
}
} else {
- if let extractor = ArticleExtractor(currentLink) {
- extractor.delegate = self
- extractor.process()
- articleExtractor = extractor
- }
+ startArticleExtractorForCurrentLink()
}
}
@@ -453,18 +449,24 @@ extension MainWindowController: SidebarDelegate {
extension MainWindowController: TimelineContainerViewControllerDelegate {
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) {
-
articleExtractor?.cancel()
articleExtractor = nil
isShowingExtractedArticle = false
-
makeToolbarValidate()
let detailState: DetailState
if let articles = articles {
- detailState = articles.count == 1 ? .article(articles.first!) : .multipleSelection
- }
- else {
+ if articles.count == 1 {
+ if articles.first?.feed?.isArticleExtractorAlwaysOn ?? false {
+ detailState = .loading
+ startArticleExtractorForCurrentLink()
+ } else {
+ detailState = .article(articles.first!)
+ }
+ } else {
+ detailState = .multipleSelection
+ }
+ } else {
detailState = .noSelection
}
@@ -833,6 +835,14 @@ private extension MainWindowController {
}
}
+
+ func startArticleExtractorForCurrentLink() {
+ if let link = currentLink, let extractor = ArticleExtractor(link) {
+ extractor.delegate = self
+ extractor.process()
+ articleExtractor = extractor
+ }
+ }
func saveSplitViewState() {
// TODO: Update this for multiple windows.
diff --git a/Mac/Resources/Assets.xcassets/articleExtractor.imageset/ArticleExtractor.pdf b/Mac/Resources/Assets.xcassets/articleExtractor.imageset/ArticleExtractor.pdf
index 3dd2e82cc..4806aaecb 100644
Binary files a/Mac/Resources/Assets.xcassets/articleExtractor.imageset/ArticleExtractor.pdf and b/Mac/Resources/Assets.xcassets/articleExtractor.imageset/ArticleExtractor.pdf differ
diff --git a/Mac/Resources/Assets.xcassets/articleExtractorError.imageset/ArticleExtractorError.pdf b/Mac/Resources/Assets.xcassets/articleExtractorError.imageset/ArticleExtractorError.pdf
index 79e7f35aa..02114937b 100644
Binary files a/Mac/Resources/Assets.xcassets/articleExtractorError.imageset/ArticleExtractorError.pdf and b/Mac/Resources/Assets.xcassets/articleExtractorError.imageset/ArticleExtractorError.pdf differ
diff --git a/Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/ArticleExtractorProgress1.pdf b/Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/ArticleExtractorProgress1.pdf
index effa04f3d..b1737c594 100644
Binary files a/Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/ArticleExtractorProgress1.pdf and b/Mac/Resources/Assets.xcassets/articleExtractorProgress1.imageset/ArticleExtractorProgress1.pdf differ
diff --git a/Mac/Resources/Assets.xcassets/articleExtractorProgress2.imageset/ArticleExtractorProgress2.pdf b/Mac/Resources/Assets.xcassets/articleExtractorProgress2.imageset/ArticleExtractorProgress2.pdf
index f4167ffd2..939dcc5cc 100644
Binary files a/Mac/Resources/Assets.xcassets/articleExtractorProgress2.imageset/ArticleExtractorProgress2.pdf and b/Mac/Resources/Assets.xcassets/articleExtractorProgress2.imageset/ArticleExtractorProgress2.pdf differ
diff --git a/Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/ArticleExtractorProgress3.pdf b/Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/ArticleExtractorProgress3.pdf
index 54a621431..d27a6269b 100644
Binary files a/Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/ArticleExtractorProgress3.pdf and b/Mac/Resources/Assets.xcassets/articleExtractorProgress3.imageset/ArticleExtractorProgress3.pdf differ
diff --git a/Mac/Resources/Assets.xcassets/articleExtractorProgress4.imageset/ArticleExtractorProgress4.pdf b/Mac/Resources/Assets.xcassets/articleExtractorProgress4.imageset/ArticleExtractorProgress4.pdf
index 670a4baab..2e27e98c6 100644
Binary files a/Mac/Resources/Assets.xcassets/articleExtractorProgress4.imageset/ArticleExtractorProgress4.pdf and b/Mac/Resources/Assets.xcassets/articleExtractorProgress4.imageset/ArticleExtractorProgress4.pdf differ
diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj
index 7181d159c..d57ac1872 100644
--- a/NetNewsWire.xcodeproj/project.pbxproj
+++ b/NetNewsWire.xcodeproj/project.pbxproj
@@ -78,6 +78,9 @@
515D4FCC2325815A00EE1167 /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; };
51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */; };
5170743A232AABFC00A461A3 /* FlattenedAccountFolderPickerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452812265093600C03939 /* FlattenedAccountFolderPickerData.swift */; };
+ 517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
+ 517630052336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
+ 517630232336657E00E15FFF /* DetailViewControllerWebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517630222336657E00E15FFF /* DetailViewControllerWebViewProvider.swift */; };
5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */; };
5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */; };
5183CCDD226F1F5C0010922C /* NavigationProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCDC226F1F5C0010922C /* NavigationProgressView.swift */; };
@@ -813,6 +816,8 @@
515D4FCD2325909200EE1167 /* NetNewsWire_iOS_ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NetNewsWire_iOS_ShareExtension.entitlements; sourceTree = ""; };
515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSshareextension_target.xcconfig; sourceTree = ""; };
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = ""; };
+ 517630032336215100E15FFF /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = ""; };
+ 517630222336657E00E15FFF /* DetailViewControllerWebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewControllerWebViewProvider.swift; sourceTree = ""; };
5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicLabel.swift; sourceTree = ""; };
5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicImageView.swift; sourceTree = ""; };
5183CCDC226F1F5C0010922C /* NavigationProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationProgressView.swift; sourceTree = ""; };
@@ -1370,6 +1375,7 @@
isa = PBXGroup;
children = (
51C4527E2265092C00C03939 /* DetailViewController.swift */,
+ 517630222336657E00E15FFF /* DetailViewControllerWebViewProvider.swift */,
515ADE3F22E11FAE006B2460 /* SystemMessageViewController.swift */,
);
path = Detail;
@@ -1393,6 +1399,7 @@
49F40DEF2335B71000552BF4 /* newsfoot.js */,
849A977D1ED9EC42007D329B /* ArticleRenderer.swift */,
848362FE2262A30E00DA1D35 /* template.html */,
+ 517630032336215100E15FFF /* main.js */,
);
path = "Article Rendering";
sourceTree = "";
@@ -2502,6 +2509,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 517630052336215100E15FFF /* main.js in Resources */,
511D43D0231FA62500FB1562 /* TimelineKeyboardShortcuts.plist in Resources */,
51C452862265093600C03939 /* Add.storyboard in Resources */,
511D43EF231FBDE900FB1562 /* LaunchScreenPad.storyboard in Resources */,
@@ -2537,6 +2545,7 @@
51EF0F8E2279C9260050506E /* AccountsAdd.xib in Resources */,
84C9FC8F22629E8F00D921D6 /* NetNewsWire.sdef in Resources */,
84C9FC7D22629E1200D921D6 /* AccountsDetail.xib in Resources */,
+ 517630042336215100E15FFF /* main.js in Resources */,
5144EA362279FC3D00D19003 /* AccountsAddLocal.xib in Resources */,
84C9FC8C22629E8F00D921D6 /* KeyboardShortcuts.html in Resources */,
5144EA3B227A379E00D19003 /* ImportOPMLSheet.xib in Resources */,
@@ -2702,6 +2711,7 @@
51C452A722650A3D00C03939 /* RSImage-Extensions.swift in Sources */,
51C45269226508F600C03939 /* MasterFeedTableViewCell.swift in Sources */,
51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */,
+ 517630232336657E00E15FFF /* DetailViewControllerWebViewProvider.swift in Sources */,
51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */,
51AF460E232488C6001742EF /* Account-Extensions.swift in Sources */,
5183CCDD226F1F5C0010922C /* NavigationProgressView.swift in Sources */,
diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift
index 5903b69f1..37f90a440 100644
--- a/Shared/Article Rendering/ArticleRenderer.swift
+++ b/Shared/Article Rendering/ArticleRenderer.swift
@@ -11,10 +11,18 @@ import RSCore
import Articles
import Account
-typealias ArticleRendering = (style: String, html: String)
-
struct ArticleRenderer {
+ typealias Rendering = (style: String, html: String)
+ typealias Page = (html: String, baseURL: URL)
+
+ static var page: Page = {
+ let pageURL = Bundle.main.url(forResource: "page", withExtension: "html")!
+ let html = try! String(contentsOf: pageURL)
+ let baseURL = pageURL.deletingLastPathComponent()
+ return Page(html: html, baseURL: baseURL)
+ }()
+
private let article: Article?
private let extractedArticle: ExtractedArticle?
private let articleStyle: ArticleStyle
@@ -38,22 +46,27 @@ struct ArticleRenderer {
// MARK: - API
- static func articleHTML(article: Article, extractedArticle: ExtractedArticle? = nil, style: ArticleStyle) -> ArticleRendering {
+ static func articleHTML(article: Article, extractedArticle: ExtractedArticle? = nil, style: ArticleStyle) -> Rendering {
let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, style: style)
return (renderer.styleString(), renderer.articleHTML)
}
- static func multipleSelectionHTML(style: ArticleStyle) -> ArticleRendering {
+ static func multipleSelectionHTML(style: ArticleStyle) -> Rendering {
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style)
return (renderer.styleString(), renderer.multipleSelectionHTML)
}
- static func noSelectionHTML(style: ArticleStyle) -> ArticleRendering {
+ static func loadingHTML(style: ArticleStyle) -> Rendering {
+ let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style)
+ return (renderer.styleString(), renderer.loadingHTML)
+ }
+
+ static func noSelectionHTML(style: ArticleStyle) -> Rendering {
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style)
return (renderer.styleString(), renderer.noSelectionHTML)
}
- static func noContentHTML(style: ArticleStyle) -> ArticleRendering {
+ static func noContentHTML(style: ArticleStyle) -> Rendering {
let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style)
return (renderer.styleString(), renderer.noContentHTML)
}
@@ -73,6 +86,11 @@ private extension ArticleRenderer {
return renderHTML(withBody: body)
}
+ private var loadingHTML: String {
+ let body = "Loading...
"
+ return renderHTML(withBody: body)
+ }
+
private var noSelectionHTML: String {
let body = "No selection
"
return renderHTML(withBody: body)
diff --git a/Shared/Article Rendering/main.js b/Shared/Article Rendering/main.js
new file mode 100644
index 000000000..e33d725da
--- /dev/null
+++ b/Shared/Article Rendering/main.js
@@ -0,0 +1,43 @@
+function mouseDidEnterLink(anchor) {
+ window.webkit.messageHandlers.mouseDidEnter.postMessage(anchor.href);
+}
+
+function mouseDidExitLink(anchor) {
+ window.webkit.messageHandlers.mouseDidExit.postMessage(anchor.href);
+}
+
+function wrapFrames() {
+ document.querySelectorAll("iframe").forEach(element => {
+ var wrapper = document.createElement("div");
+ wrapper.classList.add("iframeWrap");
+ element.parentNode.insertBefore(wrapper, element);
+ wrapper.appendChild(element);
+ });
+}
+
+function stripStyles() {
+ document.getElementsByTagName("body")[0].querySelectorAll("style, link[rel=stylesheet]").forEach(element => element.remove());
+ document.getElementsByTagName("body")[0].querySelectorAll("[style]").forEach(element => element.removeAttribute("style"));
+}
+
+function linkHover() {
+ var anchors = document.getElementsByTagName("a");
+ for (var i = 0; i < anchors.length; i++) {
+ anchors[i].addEventListener("mouseenter", function() { mouseDidEnterLink(this) });
+ anchors[i].addEventListener("mouseleave", function() { mouseDidExitLink(this) });
+ }
+}
+
+function error() {
+ document.body.innerHTML = "error";
+}
+
+function render(data) {
+ document.getElementsByTagName("style")[0].innerHTML = data.style;
+ document.body.innerHTML = data.body;
+ window.scrollTo(0, 0);
+
+ wrapFrames()
+ stripStyles()
+ linkHover()
+}
diff --git a/iOS/Detail/DetailViewController.swift b/iOS/Detail/DetailViewController.swift
index 521a17938..9878be601 100644
--- a/iOS/Detail/DetailViewController.swift
+++ b/iOS/Detail/DetailViewController.swift
@@ -40,27 +40,23 @@ class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
- 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)
-
- updateArticleSelection()
-
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil)
+
+ DetailViewControllerWebViewProvider.shared.dequeueWebView() { webView in
+
+ self.webView = webView
+ self.webViewContainer.addChildAndPin(webView)
+ webView.navigationDelegate = self
+
+ // Even though page.html should be loaded into this webview, we have to do it again
+ // to work around this bug: http://www.openradar.me/22855188
+ webView.loadHTMLString(ArticleRenderer.page.html, baseURL: ArticleRenderer.page.baseURL)
+
+ }
+
}
override func viewDidAppear(_ animated: Bool) {
@@ -243,6 +239,11 @@ extension DetailViewController: WKNavigationDelegate {
}
}
+
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+ self.updateArticleSelection()
+ }
+
}
// MARK: Private
@@ -261,60 +262,3 @@ private struct TemplateData: Codable {
let style: String
let body: String
}
-
-
-// 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()
-
- static let template: String = {
- let path = Bundle.main.path(forResource: "page", ofType: "html")!
- let s = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue)
- return s as String
- }()
-
- 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) {
- guard queue.count < maximumQueueDepth else {
- return
- }
-
- webView.uiDelegate = nil
- webView.navigationDelegate = nil
-
- webView.loadHTMLString(DetailViewControllerWebViewProvider.template, baseURL: nil)
-
- queue.insert(webView, at: 0)
- }
-
- // MARK: Private
-
- private let minimumQueueDepth = 3
- private let maximumQueueDepth = 6
- private var queue: [WKWebView] = []
-
- private init() {
- replenishQueueIfNeeded()
- }
-
- private func replenishQueueIfNeeded() {
- while queue.count < minimumQueueDepth {
- let webView = WKWebView(frame: .zero)
- enqueueWebView(webView)
- }
- }
-}
diff --git a/iOS/Detail/DetailViewControllerWebViewProvider.swift b/iOS/Detail/DetailViewControllerWebViewProvider.swift
new file mode 100644
index 000000000..35bf79b04
--- /dev/null
+++ b/iOS/Detail/DetailViewControllerWebViewProvider.swift
@@ -0,0 +1,84 @@
+//
+// DetailViewControllerWebViewProvider.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 DetailViewControllerWebViewProvider: NSObject, WKNavigationDelegate {
+
+ static let shared = DetailViewControllerWebViewProvider()
+
+ private let minimumQueueDepth = 3
+ private let maximumQueueDepth = 6
+ private var queue: [WKWebView] = []
+
+ private var waitingForFirstLoad = true
+ private var waitingCompletionHandler: ((WKWebView) -> ())?
+
+ func dequeueWebView(completion: @escaping (WKWebView) -> ()) {
+ if waitingForFirstLoad {
+ waitingCompletionHandler = completion
+ } else {
+ completeRequest(completion: completion)
+ }
+ }
+
+ func enqueueWebView(_ webView: WKWebView) {
+ guard queue.count < maximumQueueDepth else {
+ return
+ }
+
+ webView.navigationDelegate = self
+ queue.insert(webView, at: 0)
+
+ webView.loadHTMLString(ArticleRenderer.page.html, baseURL: ArticleRenderer.page.baseURL)
+
+ }
+
+ // MARK: WKNavigationDelegate
+
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+ if waitingForFirstLoad {
+ waitingForFirstLoad = false
+ if let completion = waitingCompletionHandler {
+ completeRequest(completion: completion)
+ waitingCompletionHandler = nil
+ }
+ }
+ }
+
+ // MARK: Private
+
+ private override init() {
+ super.init()
+ replenishQueueIfNeeded()
+ }
+
+ private func replenishQueueIfNeeded() {
+ while queue.count < minimumQueueDepth {
+ let webView = WKWebView(frame: .zero)
+ enqueueWebView(webView)
+ }
+ }
+
+ private func completeRequest(completion: @escaping (WKWebView) -> ()) {
+ if let webView = queue.popLast() {
+ webView.navigationDelegate = nil
+ replenishQueueIfNeeded()
+ completion(webView)
+ return
+ }
+
+ assertionFailure("Creating WKWebView in \(#function); queue has run dry.")
+ let webView = WKWebView(frame: .zero)
+ completion(webView)
+ }
+
+}
diff --git a/iOS/Resources/page.html b/iOS/Resources/page.html
index 7665aa643..4baf2cfee 100644
--- a/iOS/Resources/page.html
+++ b/iOS/Resources/page.html
@@ -6,29 +6,8 @@
color-scheme: light dark;
}
-
+