From dfd6be5fa63e45dfc5d9a7539194b26ac52a9488 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 4 Feb 2020 16:00:26 -0800 Subject: [PATCH] Flush are rebuild web view queue when suspending and restoring the app. Issue #1563 --- NetNewsWire.xcodeproj/project.pbxproj | 6 +- iOS/Article/ArticleViewController.swift | 10 +++ iOS/Article/WebViewController.swift | 20 ----- iOS/Article/WebViewProvider.swift | 90 ++++++++++++++----- iOS/Article/WrapperScriptMessageHandler.swift | 25 ++++++ iOS/Resources/main_ios.js | 4 + 6 files changed, 114 insertions(+), 41 deletions(-) create mode 100644 iOS/Article/WrapperScriptMessageHandler.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index c2c003b99..c69250af7 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -224,6 +224,7 @@ 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; 51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; }; 51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; }; + 51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */; }; 51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; }; 51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; }; 51CE1C712367721A005548FC /* testURLsOfCurrentArticle.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EADB213660A100CF2DE4 /* testURLsOfCurrentArticle.applescript */; }; @@ -1354,6 +1355,7 @@ 51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = ""; }; 51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; 51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = ""; }; + 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperScriptMessageHandler.swift; sourceTree = ""; }; 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = ""; }; 51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; }; 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = ""; }; @@ -1969,6 +1971,7 @@ isa = PBXGroup; children = ( 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */, + 51F9F3F623DF6DB200A314FD /* ArticleIconSchemeHandler.swift */, 51C4527E2265092C00C03939 /* ArticleViewController.swift */, 51C266E9238C334800F53014 /* ContextMenuPreviewViewController.swift */, 514219362352510100E07E2C /* ImageScrollView.swift */, @@ -1977,7 +1980,7 @@ 512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */, 51AB8AB223B7F4C6008F147D /* WebViewController.swift */, 517630222336657E00E15FFF /* WebViewProvider.swift */, - 51F9F3F623DF6DB200A314FD /* ArticleIconSchemeHandler.swift */, + 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */, ); path = Article; sourceTree = ""; @@ -3920,6 +3923,7 @@ 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */, 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, + 51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */, 51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */, 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */, 5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */, diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 718060997..fa245f521 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -75,6 +75,7 @@ class ArticleViewController: UIViewController { 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(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) let fullScreenTapZone = UIView() @@ -190,6 +191,15 @@ class ArticleViewController: UIViewController { } } + @objc func contentSizeCategoryDidChange(_ note: Notification) { + coordinator.webViewProvider.flushQueue() + coordinator.webViewProvider.replenishQueueIfNeeded() + if let controller = currentWebViewController { + controller.fullReload() + self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil) + } + } + @objc func willEnterForeground(_ note: Notification) { // The toolbar will come back on you if you don't hide it again if AppDefaults.articleFullscreenEnabled { diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 1442eef18..886f752e4 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -89,7 +89,6 @@ class WebViewController: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil) // Configure the tap zones configureTopShowBarsView() @@ -118,10 +117,6 @@ class WebViewController: UIViewController { reloadArticleImage() } - @objc func contentSizeCategoryDidChange(_ note: Notification) { - fullReload() - } - // MARK: Actions @objc func showBars(_ sender: Any) { @@ -354,21 +349,6 @@ extension WebViewController: WKScriptMessageHandler { } -class WrapperScriptMessageHandler: NSObject, WKScriptMessageHandler { - - // We need to wrap a message handler to prevent a circlular reference - private weak var handler: WKScriptMessageHandler? - - init(_ handler: WKScriptMessageHandler) { - self.handler = handler - } - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - handler?.userContentController(userContentController, didReceive: message) - } - -} - // MARK: UIViewControllerTransitioningDelegate extension WebViewController: UIViewControllerTransitioningDelegate { diff --git a/iOS/Article/WebViewProvider.swift b/iOS/Article/WebViewProvider.swift index 75eec147c..8fda2d206 100644 --- a/iOS/Article/WebViewProvider.swift +++ b/iOS/Article/WebViewProvider.swift @@ -13,6 +13,10 @@ import WebKit /// 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 @@ -26,7 +30,31 @@ class WebViewProvider: NSObject, WKNavigationDelegate { 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) -> ()) { @@ -42,11 +70,10 @@ class WebViewProvider: NSObject, WKNavigationDelegate { return } - webView.navigationDelegate = self + 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 @@ -63,30 +90,39 @@ class WebViewProvider: NSObject, WKNavigationDelegate { } } - // MARK: Private +} - private func replenishQueueIfNeeded() { - while queue.subviews.count < minimumQueueDepth { - let preferences = WKPreferences() - preferences.javaScriptCanOpenWindowsAutomatically = false - preferences.javaScriptEnabled = true +// MARK: WKScriptMessageHandler - let configuration = WKWebViewConfiguration() - configuration.preferences = preferences - configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs") - configuration.allowsInlineMediaPlayback = true - configuration.mediaTypesRequiringUserActionForPlayback = .video - configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) - - let webView = WKWebView(frame: .zero, configuration: configuration) - enqueueWebView(webView) +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 } } - private func completeRequest(completion: @escaping (WKWebView) -> ()) { +} + +// MARK: Private + +private extension WebViewProvider { + + func completeRequest(completion: @escaping (WKWebView) -> ()) { if let webView = queue.subviews.last as? WKWebView { webView.removeFromSuperview() - webView.navigationDelegate = nil + webView.configuration.userContentController.removeScriptMessageHandler(forName: MessageName.domContentLoaded) replenishQueueIfNeeded() completion(webView) return @@ -96,5 +132,19 @@ class WebViewProvider: NSObject, WKNavigationDelegate { 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 + } } diff --git a/iOS/Article/WrapperScriptMessageHandler.swift b/iOS/Article/WrapperScriptMessageHandler.swift new file mode 100644 index 000000000..a12c606ce --- /dev/null +++ b/iOS/Article/WrapperScriptMessageHandler.swift @@ -0,0 +1,25 @@ +// +// WrapperScriptMessageHandler.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 2/4/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation +import WebKit + +class WrapperScriptMessageHandler: NSObject, WKScriptMessageHandler { + + // We need to wrap a message handler to prevent a circlular reference + private weak var handler: WKScriptMessageHandler? + + init(_ handler: WKScriptMessageHandler) { + self.handler = handler + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + handler?.userContentController(userContentController, didReceive: message) + } + +} diff --git a/iOS/Resources/main_ios.js b/iOS/Resources/main_ios.js index 646b53d50..2945aa292 100644 --- a/iOS/Resources/main_ios.js +++ b/iOS/Resources/main_ios.js @@ -156,3 +156,7 @@ function stopMediaPlayback() { element.pause(); }); } + +window.addEventListener('DOMContentLoaded', (event) => { + window.webkit.messageHandlers.domContentLoaded.postMessage(""); +});