mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Delete PreloadedWebView and WebViewProvider. Create WebViewConfiguration for shared web view configuration code.
This commit is contained in:
@@ -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
|
||||
|
||||
76
Shared/Article Rendering/WebViewConfiguration.swift
Normal file
76
Shared/Article Rendering/WebViewConfiguration.swift
Normal 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
|
||||
}()
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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!
|
||||
|
||||
Reference in New Issue
Block a user