From b7d4041781064e25d6180626f9deaf339da101e2 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 28 Apr 2025 21:00:31 -0700 Subject: [PATCH] Simplify WebView loading on iOS. --- .../ArticleThemeDownloader.swift | 2 +- iOS/Article/PreloadedWebView.swift | 75 ------------- iOS/Article/WebViewController.swift | 102 ++++++++--------- iOS/Article/WebViewProvider.swift | 103 ------------------ iOS/MainFeed/MainFeedViewController.swift | 5 +- .../MainTimelineViewController.swift | 5 +- iOS/SceneCoordinator.swift | 2 - 7 files changed, 59 insertions(+), 235 deletions(-) delete mode 100644 iOS/Article/PreloadedWebView.swift delete mode 100644 iOS/Article/WebViewProvider.swift diff --git a/Shared/ArticleStyles/ArticleThemeDownloader.swift b/Shared/ArticleStyles/ArticleThemeDownloader.swift index ee6d719c6..43ee04567 100644 --- a/Shared/ArticleStyles/ArticleThemeDownloader.swift +++ b/Shared/ArticleStyles/ArticleThemeDownloader.swift @@ -76,7 +76,7 @@ public class ArticleThemeDownloader { if let directoryContents = FileManager.default.enumerator(atPath: searchPath) { while let file = directoryContents.nextObject() as? String { if file.hasPrefix("__MACOSX/") { - logger.debug("Ignoring theme file in __MACOSX folder.") + //logger.debug("Ignoring theme file in __MACOSX folder.") continue } if file.hasSuffix(".nnwtheme") { diff --git a/iOS/Article/PreloadedWebView.swift b/iOS/Article/PreloadedWebView.swift deleted file mode 100644 index fd407ca0b..000000000 --- a/iOS/Article/PreloadedWebView.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// PreloadedWebView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 2/25/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import WebKit - -class PreloadedWebView: WKWebView { - - private var isReady: Bool = false - private var readyCompletion: (() -> Void)? - - init(articleIconSchemeHandler: ArticleIconSchemeHandler) { - let preferences = WKPreferences() - preferences.javaScriptCanOpenWindowsAutomatically = false - - let configuration = WKWebViewConfiguration() - configuration.preferences = preferences - configuration.defaultWebpagePreferences.allowsContentJavaScript = true - configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs") - configuration.allowsInlineMediaPlayback = true - configuration.mediaTypesRequiringUserActionForPlayback = .audio - configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) - - super.init(frame: .zero, configuration: configuration) - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - } - - func preload() { - navigationDelegate = self - loadFileURL(ArticleRenderer.blank.url, allowingReadAccessTo: ArticleRenderer.blank.baseURL) - } - - func ready(completion: @escaping () -> Void) { - if isReady { - completeRequest(completion: completion) - } else { - readyCompletion = completion - } - } - -} - -// MARK: WKScriptMessageHandler - -extension PreloadedWebView: WKNavigationDelegate { - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - isReady = true - if let completion = readyCompletion { - completeRequest(completion: completion) - readyCompletion = nil - } - } - -} - -// MARK: Private - -private extension PreloadedWebView { - - func completeRequest(completion: @escaping () -> Void) { - isReady = false - navigationDelegate = nil - completion() - } - -} diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index c5048bf09..45b6b4a78 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -31,14 +31,15 @@ class WebViewController: UIViewController { private var topShowBarsViewConstraint: NSLayoutConstraint! private var bottomShowBarsViewConstraint: NSLayoutConstraint! - private var webView: PreloadedWebView? { - return view.subviews[0] as? PreloadedWebView + var webView: WKWebView? { + return view.subviews[0] as? WKWebView } - + private lazy var contextMenuInteraction = UIContextMenuInteraction(delegate: self) private var isFullScreenAvailable: Bool { return AppDefaults.shared.articleFullscreenAvailable && traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed } + private lazy var articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator); private lazy var transition = ImageTransition(controller: self) private var clickedImageCompletion: (() -> Void)? @@ -351,14 +352,6 @@ extension WebViewController: UIContextMenuInteractionDelegate { extension WebViewController: WKNavigationDelegate { - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - for (index, view) in view.subviews.enumerated() { - if index != 0, let oldWebView = view as? PreloadedWebView { - oldWebView.removeFromSuperview() - } - } - } - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if navigationAction.navigationType == .linkActivated { @@ -519,49 +512,57 @@ private extension WebViewController { return } - coordinator.webViewProvider.dequeueWebView() { webView in - - webView.ready { - - // Add the webview - webView.translatesAutoresizingMaskIntoConstraints = false - self.view.insertSubview(webView, at: 0) - NSLayoutConstraint.activate([ - self.view.leadingAnchor.constraint(equalTo: webView.leadingAnchor), - self.view.trailingAnchor.constraint(equalTo: webView.trailingAnchor), - self.view.topAnchor.constraint(equalTo: webView.topAnchor), - self.view.bottomAnchor.constraint(equalTo: webView.bottomAnchor) - ]) - - // UISplitViewController reports the wrong size to WKWebView which can cause horizontal - // rubberbanding on the iPad. This interferes with our UIPageViewController preventing - // us from easily swiping between WKWebViews. This hack fixes that. - webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: -1, bottom: 0, right: 0) + let preferences = WKPreferences() + preferences.javaScriptCanOpenWindowsAutomatically = false - webView.scrollView.setZoomScale(1.0, animated: false) + /// The defaults for `preferredContentMode` and `allowsContentJavaScript` are suitable + /// and don't need to be explicitly set. + /// `allowsContentJavaScript` replaces `WKPreferences.javascriptEnabled`. + let webpagePreferences = WKWebpagePreferences() - self.view.setNeedsLayout() - self.view.layoutIfNeeded() - - // Configure the webview - webView.navigationDelegate = self - webView.uiDelegate = self - webView.scrollView.delegate = self - self.configureContextMenuInteraction() - - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked) - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown) - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.showFeedInspector) - - self.renderPage(webView) - - } - + let configuration = WKWebViewConfiguration() + configuration.defaultWebpagePreferences = webpagePreferences + configuration.preferences = preferences + configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs") + configuration.allowsInlineMediaPlayback = true + configuration.mediaTypesRequiringUserActionForPlayback = .audio + if #available(iOS 15.4, *) { + configuration.preferences.isElementFullscreenEnabled = true } - + configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) + + let webView = WKWebView(frame: self.view.bounds, configuration: configuration) + webView.isOpaque = false; + webView.backgroundColor = .clear; + + // Add the webview - using autolayout will cause fullscreen video to fail and lose the web view + webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.view.insertSubview(webView, at: 0) + + // UISplitViewController reports the wrong size to WKWebView which can cause horizontal + // rubberbanding on the iPad. This interferes with our UIPageViewController preventing + // us from easily swiping between WKWebViews. This hack fixes that. + webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: -1, bottom: 0, right: 0) + + webView.scrollView.setZoomScale(1.0, animated: false) + + self.view.setNeedsLayout() + self.view.layoutIfNeeded() + + // Configure the webview + webView.navigationDelegate = self + webView.uiDelegate = self + webView.scrollView.delegate = self + self.configureContextMenuInteraction() + + webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked) + webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown) + webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.showFeedInspector) + + self.renderPage(webView) } - func renderPage(_ webView: PreloadedWebView?) { + func renderPage(_ webView: WKWebView?) { guard let webView = webView else { return } let theme = ArticleThemesManager.shared.currentTheme @@ -591,7 +592,8 @@ private extension WebViewController { "windowScrollY": String(windowScrollY) ] - let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions) + var html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions) + html = ArticleRenderingSpecialCases.filterHTMLIfNeeded(baseURL: rendering.baseURL, html: html) webView.loadHTMLString(html, baseURL: ArticleRenderer.page.baseURL) } diff --git a/iOS/Article/WebViewProvider.swift b/iOS/Article/WebViewProvider.swift deleted file mode 100644 index 4c49d3ed5..000000000 --- a/iOS/Article/WebViewProvider.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// WebViewProvider.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 9/21/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import Foundation -import RSCore -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 { - - private let articleIconSchemeHandler: ArticleIconSchemeHandler - private let operationQueue = MainThreadOperationQueue() - private var queue = NSMutableArray() - - init(coordinator: SceneCoordinator) { - articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator) - super.init() - replenishQueueIfNeeded() - } - - func replenishQueueIfNeeded() { - operationQueue.add(WebViewProviderReplenishQueueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler)) - } - - func dequeueWebView(completion: @escaping (PreloadedWebView) -> ()) { - operationQueue.add(WebViewProviderDequeueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler, completion: completion)) - operationQueue.add(WebViewProviderReplenishQueueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler)) - } - -} - -class WebViewProviderReplenishQueueOperation: MainThreadOperation { - - // MainThreadOperation - public var isCanceled = false - public var id: Int? - public weak var operationDelegate: MainThreadOperationDelegate? - public var name: String? = "WebViewProviderReplenishQueueOperation" - public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? - - private let minimumQueueDepth = 3 - - private var queue: NSMutableArray - private var articleIconSchemeHandler: ArticleIconSchemeHandler - - init(queue: NSMutableArray, articleIconSchemeHandler: ArticleIconSchemeHandler) { - self.queue = queue - self.articleIconSchemeHandler = articleIconSchemeHandler - } - - func run() { - while queue.count < minimumQueueDepth { - let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler) - webView.preload() - queue.insert(webView, at: 0) - } - self.operationDelegate?.operationDidComplete(self) - } - -} - -class WebViewProviderDequeueOperation: MainThreadOperation { - - // MainThreadOperation - public var isCanceled = false - public var id: Int? - public weak var operationDelegate: MainThreadOperationDelegate? - public var name: String? = "WebViewProviderFlushQueueOperation" - public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? - - private var queue: NSMutableArray - private var articleIconSchemeHandler: ArticleIconSchemeHandler - private var completion: (PreloadedWebView) -> () - - init(queue: NSMutableArray, articleIconSchemeHandler: ArticleIconSchemeHandler, completion: @escaping (PreloadedWebView) -> ()) { - self.queue = queue - self.articleIconSchemeHandler = articleIconSchemeHandler - self.completion = completion - } - - func run() { - if let webView = queue.lastObject as? PreloadedWebView { - self.completion(webView) - self.queue.remove(webView) - self.operationDelegate?.operationDidComplete(self) - return - } - - assertionFailure("Creating PreloadedWebView in \(#function); queue has run dry.") - - let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler) - webView.preload() - self.completion(webView) - self.operationDelegate?.operationDidComplete(self) - } - -} diff --git a/iOS/MainFeed/MainFeedViewController.swift b/iOS/MainFeed/MainFeedViewController.swift index db4af7b9e..4ecef0bdf 100644 --- a/iOS/MainFeed/MainFeedViewController.swift +++ b/iOS/MainFeed/MainFeedViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import WebKit import Account import Articles import RSCore @@ -34,10 +35,10 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner { private let keyboardManager = KeyboardManager(type: .sidebar) override var keyCommands: [UIKeyCommand]? { - // If the first responder is the WKWebView (PreloadedWebView) we don't want to supply any keyboard + // If the first responder is the WKWebView we don't want to supply any keyboard // commands that the system is looking for by going up the responder chain. They will interfere with // the WKWebViews built in hardware keyboard shortcuts, specifically the up and down arrow keys. - guard let current = UIResponder.currentFirstResponder, !(current is PreloadedWebView) else { return nil } + guard let current = UIResponder.currentFirstResponder, !(current is WKWebView) else { return nil } return keyboardManager.keyCommands } diff --git a/iOS/MainTimeline/MainTimelineViewController.swift b/iOS/MainTimeline/MainTimelineViewController.swift index 4f39075ce..06853e1bd 100644 --- a/iOS/MainTimeline/MainTimelineViewController.swift +++ b/iOS/MainTimeline/MainTimelineViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import WebKit import RSCore import RSWeb import Account @@ -97,10 +98,10 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner { private let keyboardManager = KeyboardManager(type: .timeline) override var keyCommands: [UIKeyCommand]? { - // If the first responder is the WKWebView (PreloadedWebView) we don't want to supply any keyboard + // If the first responder is the WKWebView we don't want to supply any keyboard // commands that the system is looking for by going up the responder chain. They will interfere with // the WKWebViews built in hardware keyboard shortcuts, specifically the up and down arrow keys. - guard let current = UIResponder.currentFirstResponder, !(current is PreloadedWebView) else { return nil } + guard let current = UIResponder.currentFirstResponder, !(current is WKWebView) else { return nil } return keyboardManager.keyCommands } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 259ba1b24..921adb8d1 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -46,8 +46,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { return rootSplitViewController.undoManager } - lazy var webViewProvider = WebViewProvider(coordinator: self) - private var activityManager = ActivityManager() private var rootSplitViewController: RootSplitViewController!