From 1db92f75a4d4598f362bb9ad14d81a56210301e8 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 15 Jan 2025 21:55:09 -0800 Subject: [PATCH] Delete PreloadedWebView and WebViewProvider. Create WebViewConfiguration for shared web view configuration code. --- .../Detail/DetailWebViewController.swift | 27 +---- .../WebViewConfiguration.swift | 76 +++++++++++++ iOS/AppDefaults.swift | 12 +- iOS/Article/PreloadedWebView.swift | 75 ------------- iOS/Article/WebViewController.swift | 85 ++++++--------- iOS/Article/WebViewProvider.swift | 103 ------------------ iOS/MainFeed/MainFeedViewController.swift | 5 +- iOS/MainTimeline/TimelineViewController.swift | 5 +- iOS/SceneCoordinator.swift | 2 - 9 files changed, 128 insertions(+), 262 deletions(-) create mode 100644 Shared/Article Rendering/WebViewConfiguration.swift delete mode 100644 iOS/Article/PreloadedWebView.swift delete mode 100644 iOS/Article/WebViewProvider.swift diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index a4bf3399d..91700cd9e 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -73,16 +73,6 @@ final class DetailWebViewController: NSViewController { } } - static let userScripts: [WKUserScript] = { - let filenames = ["main", "main_mac", "newsfoot"] - let scripts = filenames.map { filename in - let scriptURL = Bundle.main.url(forResource: filename, withExtension: ".js")! - let scriptSource = try! String(contentsOf: scriptURL, encoding: .utf8) - return WKUserScript(source: scriptSource, injectionTime: .atDocumentStart, forMainFrameOnly: true) - } - return scripts - }() - private struct MessageName { static let mouseDidEnter = "mouseDidEnter" static let mouseDidExit = "mouseDidExit" @@ -90,23 +80,8 @@ final class DetailWebViewController: NSViewController { } override func loadView() { - let preferences = WKPreferences() - preferences.minimumFontSize = 12.0 - preferences.javaScriptCanOpenWindowsAutomatically = false - let configuration = WKWebViewConfiguration() - configuration.preferences = preferences - configuration.defaultWebpagePreferences.allowsContentJavaScript = AppDefaults.shared.isArticleContentJavascriptEnabled - configuration.setURLSchemeHandler(detailIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) - - let userContentController = WKUserContentController() - userContentController.add(self, name: MessageName.windowDidScroll) - userContentController.add(self, name: MessageName.mouseDidEnter) - userContentController.add(self, name: MessageName.mouseDidExit) - for script in Self.userScripts { - userContentController.addUserScript(script) - } - configuration.userContentController = userContentController + let configuration = WebViewConfiguration.configuration(with: detailIconSchemeHandler) webView = DetailWebView(frame: NSRect.zero, configuration: configuration) webView.uiDelegate = self diff --git a/Shared/Article Rendering/WebViewConfiguration.swift b/Shared/Article Rendering/WebViewConfiguration.swift new file mode 100644 index 000000000..61f5f1979 --- /dev/null +++ b/Shared/Article Rendering/WebViewConfiguration.swift @@ -0,0 +1,76 @@ +// +// WebViewConfiguration.swift +// NetNewsWire +// +// Created by Brent Simmons on 1/15/25. +// Copyright © 2025 Ranchero Software. All rights reserved. +// + +import Foundation +import WebKit + +final class WebViewConfiguration { + + static func configuration(with urlSchemeHandler: WKURLSchemeHandler) -> WKWebViewConfiguration { + + let configuration = WKWebViewConfiguration() + + configuration.preferences = preferences + configuration.defaultWebpagePreferences = webpagePreferences + configuration.mediaTypesRequiringUserActionForPlayback = .all + configuration.setURLSchemeHandler(urlSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) + configuration.userContentController = userContentController + +#if os(iOS) + configuration.allowsInlineMediaPlayback = true +#endif + + return configuration + } +} + +private extension WebViewConfiguration { + + static var preferences: WKPreferences { + + let preferences = WKPreferences() + preferences.javaScriptCanOpenWindowsAutomatically = false + preferences.minimumFontSize = 12 + +#if os(iOS) + preferences.isElementFullscreenEnabled = true +#endif + + return preferences + } + + static var webpagePreferences: WKWebpagePreferences { + let preferences = WKWebpagePreferences() + preferences.allowsContentJavaScript = AppDefaults.shared.isArticleContentJavascriptEnabled + return preferences + } + + static var userContentController: WKUserContentController { + let userContentController = WKUserContentController() + for script in articleScripts { + userContentController.addUserScript(script) + } + return userContentController + } + + static let articleScripts: [WKUserScript] = { + +#if os(iOS) + let filenames = ["main", "main_ios", "newsfoot"] +#else + let filenames = ["main", "main_mac", "newsfoot"] +#endif + + let scripts = filenames.map { filename in + let scriptURL = Bundle.main.url(forResource: filename, withExtension: ".js")! + let scriptSource = try! String(contentsOf: scriptURL, encoding: .utf8) + return WKUserScript(source: scriptSource, injectionTime: .atDocumentStart, forMainFrameOnly: true) + } + return scripts + }() +} diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index 149b8c24d..5f676b8fc 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -57,6 +57,7 @@ final class AppDefaults { static let addFolderAccountID = "addFolderAccountID" static let useSystemBrowser = "useSystemBrowser" static let currentThemeName = "currentThemeName" + static let articleContentJavascriptEnabled = "articleContentJavascriptEnabled" } let isDeveloperBuild: Bool = { @@ -225,7 +226,16 @@ final class AppDefaults { AppDefaults.setString(for: Key.currentThemeName, newValue) } } - + + var isArticleContentJavascriptEnabled: Bool { + get { + UserDefaults.standard.bool(forKey: Key.articleContentJavascriptEnabled) + } + set { + UserDefaults.standard.set(newValue, forKey: Key.articleContentJavascriptEnabled) + } + } + static func registerDefaults() { let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue, Key.timelineGroupByFeed: false, 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 64c78aa94..258a7c2c2 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 { @@ -513,55 +506,45 @@ private extension WebViewController { func loadWebView(replaceExistingWebView: Bool = false) { guard isViewLoaded else { return } - - if !replaceExistingWebView, let webView = webView { + + if !replaceExistingWebView, let webView { self.renderPage(webView) 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) - webView.scrollView.setZoomScale(1.0, animated: false) + let configuration = WebViewConfiguration.configuration(with: articleIconSchemeHandler) - self.view.setNeedsLayout() - self.view.layoutIfNeeded() + let webView = WKWebView(frame: self.view.bounds, configuration: configuration) + webView.isOpaque = false; + webView.backgroundColor = .clear; - // Configure the webview - webView.navigationDelegate = self - webView.uiDelegate = self - webView.scrollView.delegate = self - self.configureContextMenuInteraction() + // 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) - 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) + // 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) - self.renderPage(webView) - - } - - } - + webView.scrollView.setZoomScale(1.0, animated: false) + + self.view.setNeedsLayout() + + // 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 @@ -592,7 +575,7 @@ private extension WebViewController { ] let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions) - webView.loadHTMLString(html, baseURL: ArticleRenderer.page.baseURL) + webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL)) } func finalScrollPosition(scrollingUp: Bool) -> CGFloat { 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 a67fa074d..2d9100649 100644 --- a/iOS/MainFeed/MainFeedViewController.swift +++ b/iOS/MainFeed/MainFeedViewController.swift @@ -12,6 +12,7 @@ import Articles import RSCore import RSTree import SafariServices +import WebKit class MainFeedViewController: UITableViewController, UndoableCommandRunner { @@ -29,10 +30,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/TimelineViewController.swift b/iOS/MainTimeline/TimelineViewController.swift index 0c3cfe041..c54132bbb 100644 --- a/iOS/MainTimeline/TimelineViewController.swift +++ b/iOS/MainTimeline/TimelineViewController.swift @@ -10,6 +10,7 @@ import UIKit import RSCore import Account import Articles +import WebKit class TimelineViewController: UITableViewController, UndoableCommandRunner { @@ -34,10 +35,10 @@ class TimelineViewController: 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 e89dc71e1..c649cf084 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!