From 79c2f4c7f51541aae4d69d1e21d700bd9482ac6f Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 1 Jul 2024 22:20:00 -0700 Subject: [PATCH] Fix several concurrency warnings. --- iOS/Article/ArticleIconSchemeHandler.swift | 43 +++--- iOS/Article/WebViewController.swift | 162 +++++++++++++-------- 2 files changed, 122 insertions(+), 83 deletions(-) diff --git a/iOS/Article/ArticleIconSchemeHandler.swift b/iOS/Article/ArticleIconSchemeHandler.swift index efcab2931..d50786b35 100644 --- a/iOS/Article/ArticleIconSchemeHandler.swift +++ b/iOS/Article/ArticleIconSchemeHandler.swift @@ -8,11 +8,11 @@ import Foundation import WebKit -@preconcurrency import Images +import Images protocol ArticleIconSchemeHandlerDelegate: AnyObject { - func iconImage(for articleID: String) -> IconImage? + @MainActor func iconImage(for articleID: String) -> IconImage? } final class ArticleIconSchemeHandler: NSObject, WKURLSchemeHandler { @@ -34,31 +34,34 @@ final class ArticleIconSchemeHandler: NSObject, WKURLSchemeHandler { return } let articleID = components.path - guard let iconImage = delegate.iconImage(for: articleID) else { - urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) - return - } - Task { @MainActor in - - let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) - iconView.iconImage = iconImage - let renderedImage = iconView.asImage() - - guard let data = renderedImage.dataRepresentation() else { + MainActor.assumeIsolated { + guard let iconImage = delegate.iconImage(for: articleID) else { urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) return } - - let headerFields = ["Cache-Control": "no-cache"] - if let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: headerFields) { - urlSchemeTask.didReceive(response) - urlSchemeTask.didReceive(data) - urlSchemeTask.didFinish() + + Task { @MainActor in + + let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) + iconView.iconImage = iconImage + let renderedImage = iconView.asImage() + + guard let data = renderedImage.dataRepresentation() else { + urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) + return + } + + let headerFields = ["Cache-Control": "no-cache"] + if let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: headerFields) { + urlSchemeTask.didReceive(response) + urlSchemeTask.didReceive(data) + urlSchemeTask.didFinish() + } } } } - + func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { urlSchemeTask.didFailWithError(URLError(.unknown)) } diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index 15eb82215..365bf5780 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -15,6 +15,7 @@ import MessageUI import Core import ArticleExtractor import Images +import Web protocol WebViewControllerDelegate: AnyObject { @@ -388,81 +389,110 @@ extension WebViewController: UIContextMenuInteractionDelegate { extension WebViewController: WKNavigationDelegate { - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - - if navigationAction.navigationType == .linkActivated { - guard let url = navigationAction.request.url else { - decisionHandler(.allow) + nonisolated func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + + guard navigationAction.navigationType == .linkActivated else { + decisionHandler(.allow) + return + } + guard let url = navigationAction.request.url else { + decisionHandler(.allow) + return + } + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let scheme = components.scheme else { + decisionHandler(.allow) + return + } + + switch scheme { + + case URLScheme.http, URLScheme.https: + decisionHandler(.cancel) + openURLInBrowser(url) + + case URLScheme.mailto: + decisionHandler(.cancel) + guard let emailAddressURL = url.percentEncodedEmailAddress else { return } - - let components = URLComponents(url: url, resolvingAgainstBaseURL: false) - if components?.scheme == "http" || components?.scheme == "https" { - decisionHandler(.cancel) - if AppDefaults.shared.useSystemBrowser { - UIApplication.shared.open(url, options: [:]) - } else { - UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { didOpen in - guard didOpen == false else { - return - } - let vc = SFSafariViewController(url: url) - self.present(vc, animated: true) - } - } - - } 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 { - let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), message: NSLocalizedString("This device cannot send emails.", comment: "This device cannot send emails."), preferredStyle: .alert) - alert.addAction(.init(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil)) - self.present(alert, animated: true, completion: nil) - } - } else if components?.scheme == "tel" { - decisionHandler(.cancel) - - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [.universalLinksOnly : false], completionHandler: nil) - } - - } else { - decisionHandler(.allow) - } - } else { + openEmailAddressURL(emailAddressURL) + + case URLScheme.tel: + decisionHandler(.cancel) + openTelURL(url) + + default: decisionHandler(.allow) } } - func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { - fullReload() + nonisolated func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + + Task { @MainActor in + fullReload() + } + } + + nonisolated private func openURLInBrowser(_ url: URL) { + + Task { @MainActor in + if AppDefaults.shared.useSystemBrowser { + UIApplication.shared.open(url, options: [:]) + } else { + UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { didOpen in + guard didOpen == false else { + return + } + let vc = SFSafariViewController(url: url) + self.present(vc, animated: true) + } + } + } + } + + nonisolated private func openEmailAddressURL(_ url: URL) { + + Task { @MainActor in + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [.universalLinksOnly : false], completionHandler: nil) + } else { + let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), message: NSLocalizedString("This device cannot send emails.", comment: "This device cannot send emails."), preferredStyle: .alert) + alert.addAction(.init(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil)) + self.present(alert, animated: true, completion: nil) + } + } + } + + nonisolated private func openTelURL(_ url: URL) { + + Task { @MainActor in + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [.universalLinksOnly : false], completionHandler: nil) + } + } } - } // MARK: WKUIDelegate extension WebViewController: WKUIDelegate { - func webView(_ webView: WKWebView, contextMenuForElement elementInfo: WKContextMenuElementInfo, willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) { + nonisolated func webView(_ webView: WKWebView, contextMenuForElement elementInfo: WKContextMenuElementInfo, willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) { // We need to have at least an unimplemented WKUIDelegate assigned to the WKWebView. This makes the // link preview launch Safari when the link preview is tapped. In theory, you should be able to get // the link from the elementInfo above and transition to SFSafariViewController instead of launching // Safari. As the time of this writing, the link in elementInfo is always nil. ¯\_(ツ)_/¯ } - func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + nonisolated func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { guard let url = navigationAction.request.url else { return nil } - openURL(url) + Task { @MainActor in + openURL(url) + } + return nil } @@ -472,18 +502,24 @@ extension WebViewController: WKUIDelegate { extension WebViewController: WKScriptMessageHandler { - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - switch message.name { - case MessageName.imageWasShown: - clickedImageCompletion?() - case MessageName.imageWasClicked: - imageWasClicked(body: message.body as? String) - case MessageName.showFeedInspector: - if let feed = article?.feed { - coordinator.showFeedInspector(for: feed) + nonisolated func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + + let name = message.name + let body = message.body as? String + + Task { @MainActor in + switch name { + case MessageName.imageWasShown: + clickedImageCompletion?() + case MessageName.imageWasClicked: + imageWasClicked(body: body) + case MessageName.showFeedInspector: + if let feed = article?.feed { + coordinator.showFeedInspector(for: feed) + } + default: + return } - default: - return } }