diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 1a904bc39..eea021437 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -213,7 +213,6 @@ 51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */; }; 517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; 517630052336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; - 517630232336657E00E15FFF /* WebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517630222336657E00E15FFF /* WebViewProvider.swift */; }; 517A745B2443665000B553B9 /* UIPageViewController-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517A745A2443665000B553B9 /* UIPageViewController-Extensions.swift */; }; 5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */; }; 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */; }; @@ -358,7 +357,6 @@ 51DC07992552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; 51DC079A2552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; 51DC07AC255209E200A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; - 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */; }; 51DEE81226FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; }; 51DEE81326FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; }; 51DEE81426FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; }; @@ -1192,7 +1190,6 @@ 516AE9DE2372269A007DEEAA /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = ""; }; 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = ""; }; 517630032336215100E15FFF /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = ""; }; - 517630222336657E00E15FFF /* WebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProvider.swift; sourceTree = ""; }; 517A745A2443665000B553B9 /* UIPageViewController-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIPageViewController-Extensions.swift"; sourceTree = ""; }; 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicLabel.swift; sourceTree = ""; }; 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicImageView.swift; sourceTree = ""; }; @@ -1269,7 +1266,6 @@ 51D6A5BB23199C85001C27D8 /* TimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDataSource.swift; sourceTree = ""; }; 51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = ""; }; 51DC07972552083500A3F79F /* ArticleTextSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleTextSize.swift; sourceTree = ""; }; - 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.swift; sourceTree = ""; }; 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Appanoose.nnwtheme; sourceTree = ""; }; 51DEE81726FBFF84006DAA56 /* Promenade.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Promenade.nnwtheme; sourceTree = ""; }; 51E36E70239D6610006F47A5 /* AddFeedSelectFolderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedSelectFolderTableViewCell.swift; sourceTree = ""; }; @@ -2018,9 +2014,7 @@ 518651D9235621840078E021 /* ImageTransition.swift */, 5142192923522B5500E07E2C /* ImageViewController.swift */, 512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */, - 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */, 51AB8AB223B7F4C6008F147D /* WebViewController.swift */, - 517630222336657E00E15FFF /* WebViewProvider.swift */, 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */, D3A398632465054F00F9A366 /* FindInArticleActivity.swift */, D3555BF324664539005E48C3 /* ArticleSearchBar.swift */, @@ -3944,7 +3938,6 @@ 511B9807237DCAC90028BCAA /* UserInfoKey.swift in Sources */, 51C45269226508F600C03939 /* FeedTableViewCell.swift in Sources */, 51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */, - 517630232336657E00E15FFF /* WebViewProvider.swift in Sources */, 51E43962238037C400015C31 /* AddFeedFolderViewController.swift in Sources */, 51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */, 51FD413B2342BD0500880194 /* TimelineUnreadCountView.swift in Sources */, @@ -4053,7 +4046,6 @@ 51627A6923861DED007B3B4B /* FeedsViewController+Drop.swift in Sources */, 514219372352510100E07E2C /* ImageScrollView.swift in Sources */, 516AE9B32371C372007DEEAA /* FeedTableViewSectionHeaderLayout.swift in Sources */, - 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */, D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */, 8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */, B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */, diff --git a/iOS/Article/PreloadedWebView.swift b/iOS/Article/PreloadedWebView.swift deleted file mode 100644 index a3d3ca1aa..000000000 --- a/iOS/Article/PreloadedWebView.swift +++ /dev/null @@ -1,76 +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 - preferences.javaScriptEnabled = true - - let configuration = WKWebViewConfiguration() - configuration.preferences = preferences - 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 082ea4a5f..f03775035 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)? @@ -374,14 +375,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 { @@ -537,55 +530,77 @@ private extension WebViewController { func loadWebView(replaceExistingWebView: Bool = false) { guard isViewLoaded else { return } - + if !replaceExistingWebView, let webView = 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 preferences = WKPreferences() + preferences.javaScriptCanOpenWindowsAutomatically = false - self.view.setNeedsLayout() - self.view.layoutIfNeeded() + /// The defaults for `preferredContentMode` and `allowsContentJavaScript` are suitable + /// and don't need to be explicitly set. + /// `allowsContentJavaScript` replaces `WKPreferences.javascriptEnabled`. + let webpagePreferences = WKWebpagePreferences() - // 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 userContentController = WKUserContentController() + let baseURL = ArticleRenderer.page.baseURL + let appScriptsWorld = WKContentWorld.world(name: "NetNewsWire") + for fileName in ["main.js", "main_ios.js", "newsfoot.js"] { + userContentController.addUserScript( + .init(source: try! String(contentsOf: baseURL.appending(path: fileName, + directoryHint: .notDirectory)), + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + in: appScriptsWorld)) + } + + configuration.userContentController = userContentController + + 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 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/Feeds/FeedsViewController.swift b/iOS/Feeds/FeedsViewController.swift index f19911a51..ea06ea63a 100644 --- a/iOS/Feeds/FeedsViewController.swift +++ b/iOS/Feeds/FeedsViewController.swift @@ -29,11 +29,11 @@ class FeedsViewController: 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/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 775cf9af3..841a917ec 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -52,8 +52,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { return rootSplitViewController.undoManager } - lazy var webViewProvider = WebViewProvider(coordinator: self) - private var panelMode: PanelMode = .unset private var activityManager = ActivityManager() diff --git a/iOS/Timeline/TimelineViewController.swift b/iOS/Timeline/TimelineViewController.swift index 609823b38..951665e21 100644 --- a/iOS/Timeline/TimelineViewController.swift +++ b/iOS/Timeline/TimelineViewController.swift @@ -34,11 +34,11 @@ 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 }