Delete PreloadedWebView and WebViewProvider. Create WebViewConfiguration for shared web view configuration code.

This commit is contained in:
Brent Simmons
2025-01-15 21:55:09 -08:00
parent 7322301be3
commit 1db92f75a4
9 changed files with 128 additions and 262 deletions

View File

@@ -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

View File

@@ -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
}()
}

View File

@@ -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,

View File

@@ -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()
}
}

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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!