Revert webview changes that might be the cause of the no-scroll-on-coming-back-from-sleep bug.

This commit is contained in:
Brent Simmons
2025-05-08 21:29:58 -07:00
parent 112125e50e
commit ee9c14783f
6 changed files with 246 additions and 66 deletions

View File

@@ -0,0 +1,75 @@
//
// 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,8 +31,8 @@ class WebViewController: UIViewController {
private var topShowBarsViewConstraint: NSLayoutConstraint!
private var bottomShowBarsViewConstraint: NSLayoutConstraint!
var webView: WKWebView? {
return view.subviews[0] as? WKWebView
private var webView: PreloadedWebView? {
return view.subviews[0] as? PreloadedWebView
}
private lazy var contextMenuInteraction = UIContextMenuInteraction(delegate: self)
@@ -352,14 +352,22 @@ 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 {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if components?.scheme == "http" || components?.scheme == "https" {
decisionHandler(.cancel)
@@ -373,14 +381,14 @@ extension WebViewController: WKNavigationDelegate {
self.openURLInSafariViewController(url)
}
}
} else if components?.scheme == "mailto" {
decisionHandler(.cancel)
guard let emailAddress = url.percentEncodedEmailAddress else {
return
}
if UIApplication.shared.canOpenURL(emailAddress) {
UIApplication.shared.open(emailAddress, options: [.universalLinksOnly : false], completionHandler: nil)
} else {
@@ -390,11 +398,11 @@ extension WebViewController: WKNavigationDelegate {
}
} else if components?.scheme == "tel" {
decisionHandler(.cancel)
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [.universalLinksOnly : false], completionHandler: nil)
}
} else {
decisionHandler(.allow)
}
@@ -406,7 +414,7 @@ extension WebViewController: WKNavigationDelegate {
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
fullReload()
}
}
// MARK: WKUIDelegate
@@ -506,63 +514,55 @@ private extension WebViewController {
func loadWebView(replaceExistingWebView: Bool = false) {
guard isViewLoaded else { return }
if !replaceExistingWebView, let webView = webView {
self.renderPage(webView)
return
}
let preferences = WKPreferences()
preferences.javaScriptCanOpenWindowsAutomatically = false
/// The defaults for `preferredContentMode` and `allowsContentJavaScript` are suitable
/// and don't need to be explicitly set.
/// `allowsContentJavaScript` replaces `WKPreferences.javascriptEnabled`.
let webpagePreferences = WKWebpagePreferences()
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)
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: WKWebView?) {
func renderPage(_ webView: PreloadedWebView?) {
guard let webView = webView else { return }
let theme = ArticleThemesManager.shared.currentTheme

View File

@@ -0,0 +1,103 @@
//
// 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

@@ -35,10 +35,10 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
private let keyboardManager = KeyboardManager(type: .sidebar)
override var keyCommands: [UIKeyCommand]? {
// If the first responder is the WKWebView we don't want to supply any keyboard
// If the first responder is the WKWebView (PreloadedWebView) 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 WKWebView) else { return nil }
guard let current = UIResponder.currentFirstResponder, !(current is PreloadedWebView) else { return nil }
return keyboardManager.keyCommands
}

View File

@@ -98,11 +98,11 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
private let keyboardManager = KeyboardManager(type: .timeline)
override var keyCommands: [UIKeyCommand]? {
// If the first responder is the WKWebView we don't want to supply any keyboard
// If the first responder is the WKWebView (PreloadedWebView) 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 WKWebView) else { return nil }
guard let current = UIResponder.currentFirstResponder, !(current is PreloadedWebView) else { return nil }
return keyboardManager.keyCommands
}

View File

@@ -40,20 +40,22 @@ struct FeedNode: Hashable {
}
class SceneCoordinator: NSObject, UndoableCommandRunner {
var undoableCommands = [UndoableCommand]()
var undoManager: UndoManager? {
return rootSplitViewController.undoManager
}
lazy var webViewProvider = WebViewProvider(coordinator: self)
private var activityManager = ActivityManager()
private var rootSplitViewController: RootSplitViewController!
private var mainFeedViewController: MainFeedViewController!
private var mainTimelineViewController: MainTimelineViewController?
private var articleViewController: ArticleViewController?
private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
private let rebuildBackingStoresQueue = CoalescingQueue(name: "Rebuild The Backing Stores", interval: 0.5)
private var fetchSerialNumber = 0