diff --git a/ios/Sources/GutenbergKit/Sources/Cache/CachedAssetSchemeHandler.swift b/ios/Sources/GutenbergKit/Sources/Cache/CachedAssetSchemeHandler.swift new file mode 100644 index 00000000..6b57f9ca --- /dev/null +++ b/ios/Sources/GutenbergKit/Sources/Cache/CachedAssetSchemeHandler.swift @@ -0,0 +1,108 @@ +import Foundation +import WebKit + +class CachedAssetSchemeHandler: NSObject, WKURLSchemeHandler { + nonisolated static let cachedURLSchemePrefix = "gbk-cache-" + nonisolated static let supportedURLSchemes = ["gbk-cache-http", "gbk-cache-https"] + + nonisolated static func originalHTTPURL(from url: URL) -> URL? { + guard let scheme = url.scheme, supportedURLSchemes.contains(scheme) else { return nil } + + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { + return nil + } + + components.scheme = String(scheme.suffix(from: scheme.index(scheme.startIndex, offsetBy: cachedURLSchemePrefix.count))) + return components.url + } + + nonisolated static func cachedURL(forWebLink link: String) -> String? { + if link.starts(with: "http://") || link.starts(with: "https://") { + return cachedURLSchemePrefix + link + } + return nil + } + + let worker: Worker + + init(library: EditorAssetsLibrary) { + self.worker = .init(library: library) + } + + func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) { + Task { + await worker.start(urlSchemeTask) + } + } + + func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) { + Task { + await worker.stop(urlSchemeTask) + } + } + + actor Worker { + struct TaskInfo { + var webViewTask: WKURLSchemeTask + var fetchAssetTask: Task + + func cancel() { + fetchAssetTask.cancel() + } + } + + let library: EditorAssetsLibrary + var tasks: [ObjectIdentifier: TaskInfo] = [:] + + init(library: EditorAssetsLibrary) { + self.library = library + } + + deinit { + for (_, task) in tasks { + task.cancel() + } + } + + func start(_ task: WKURLSchemeTask) { + guard let url = task.request.url, let httpURL = CachedAssetSchemeHandler.originalHTTPURL(from: url) else { + task.didFailWithError(URLError(.badURL)) + return + } + + let taskKey = ObjectIdentifier(task) + + let fetchAssetTask = Task { [library, weak self] in + do { + let (response, content) = try await library.cacheAsset(from: httpURL, webViewURL: url) + + await self?.tasks[taskKey]?.webViewTask.didReceive(response) + await self?.tasks[taskKey]?.webViewTask.didReceive(content) + + await self?.finish(with: nil, taskKey: taskKey) + } catch { + await self?.finish(with: error, taskKey: taskKey) + } + } + tasks[taskKey] = .init(webViewTask: task, fetchAssetTask: fetchAssetTask) + } + + func stop(_ task: WKURLSchemeTask) { + let taskKey = ObjectIdentifier(task) + tasks[taskKey]?.cancel() + tasks[taskKey] = nil + } + + private func finish(with error: Error?, taskKey: ObjectIdentifier) { + guard let task = tasks[taskKey] else { return } + + if let error { + task.webViewTask.didFailWithError(error) + } else { + task.webViewTask.didFinish() + } + tasks[taskKey] = nil + } + } +} + diff --git a/ios/Sources/GutenbergKit/Sources/EditorAssetsLibrary.swift b/ios/Sources/GutenbergKit/Sources/Cache/EditorAssetsLibrary.swift similarity index 100% rename from ios/Sources/GutenbergKit/Sources/EditorAssetsLibrary.swift rename to ios/Sources/GutenbergKit/Sources/Cache/EditorAssetsLibrary.swift diff --git a/ios/Sources/GutenbergKit/Sources/Cache/EditorAssetsProvider.swift b/ios/Sources/GutenbergKit/Sources/Cache/EditorAssetsProvider.swift new file mode 100644 index 00000000..b402cb95 --- /dev/null +++ b/ios/Sources/GutenbergKit/Sources/Cache/EditorAssetsProvider.swift @@ -0,0 +1,30 @@ +import Foundation +import WebKit + +class EditorAssetsProvider: NSObject, WKScriptMessageHandlerWithReply { + let library: EditorAssetsLibrary + + init(library: EditorAssetsLibrary) { + self.library = library + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage, replyHandler: @escaping @MainActor @Sendable (Any?, String?) -> Void) { + guard let payload = message.body as? NSDictionary, + let asset = payload.object(forKey: "asset") as? String, + asset == "manifest" + else { + replyHandler(nil, "Unexpected message") + return + } + + Task.detached { [library] in + do { + let data = try await library.manifestContentForEditor() + let dict = try JSONSerialization.jsonObject(with: data) + await replyHandler(dict, nil) + } catch { + await replyHandler(nil, error.localizedDescription) + } + } + } +} diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift index e7d13de4..99c17876 100644 --- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift +++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift @@ -423,136 +423,3 @@ private final class GutenbergEditorController: NSObject, WKNavigationDelegate, W } } } - -private class EditorAssetsProvider: NSObject, WKScriptMessageHandlerWithReply { - let library: EditorAssetsLibrary - - init(library: EditorAssetsLibrary) { - self.library = library - } - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage, replyHandler: @escaping @MainActor @Sendable (Any?, String?) -> Void) { - guard let payload = message.body as? NSDictionary, - let asset = payload.object(forKey: "asset") as? String, - asset == "manifest" - else { - replyHandler(nil, "Unexpected message") - return - } - - Task.detached { [library] in - do { - let data = try await library.manifestContentForEditor() - let dict = try JSONSerialization.jsonObject(with: data) - await replyHandler(dict, nil) - } catch { - await replyHandler(nil, error.localizedDescription) - } - } - } -} - -class CachedAssetSchemeHandler: NSObject, WKURLSchemeHandler { - nonisolated static let cachedURLSchemePrefix = "gbk-cache-" - nonisolated static let supportedURLSchemes = ["gbk-cache-http", "gbk-cache-https"] - - nonisolated static func originalHTTPURL(from url: URL) -> URL? { - guard let scheme = url.scheme, supportedURLSchemes.contains(scheme) else { return nil } - - guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { - return nil - } - - components.scheme = String(scheme.suffix(from: scheme.index(scheme.startIndex, offsetBy: cachedURLSchemePrefix.count))) - return components.url - } - - nonisolated static func cachedURL(forWebLink link: String) -> String? { - if link.starts(with: "http://") || link.starts(with: "https://") { - return cachedURLSchemePrefix + link - } - return nil - } - - let worker: Worker - - init(library: EditorAssetsLibrary) { - self.worker = .init(library: library) - } - - func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) { - Task { - await worker.start(urlSchemeTask) - } - } - - func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) { - Task { - await worker.stop(urlSchemeTask) - } - } - - actor Worker { - struct TaskInfo { - var webViewTask: WKURLSchemeTask - var fetchAssetTask: Task - - func cancel() { - fetchAssetTask.cancel() - } - } - - let library: EditorAssetsLibrary - var tasks: [ObjectIdentifier: TaskInfo] = [:] - - init(library: EditorAssetsLibrary) { - self.library = library - } - - deinit { - for (_, task) in tasks { - task.cancel() - } - } - - func start(_ task: WKURLSchemeTask) { - guard let url = task.request.url, let httpURL = CachedAssetSchemeHandler.originalHTTPURL(from: url) else { - task.didFailWithError(URLError(.badURL)) - return - } - - let taskKey = ObjectIdentifier(task) - - let fetchAssetTask = Task { [library, weak self] in - do { - let (response, content) = try await library.cacheAsset(from: httpURL, webViewURL: url) - - await self?.tasks[taskKey]?.webViewTask.didReceive(response) - await self?.tasks[taskKey]?.webViewTask.didReceive(content) - - await self?.finish(with: nil, taskKey: taskKey) - } catch { - await self?.finish(with: error, taskKey: taskKey) - } - } - tasks[taskKey] = .init(webViewTask: task, fetchAssetTask: fetchAssetTask) - } - - func stop(_ task: WKURLSchemeTask) { - let taskKey = ObjectIdentifier(task) - tasks[taskKey]?.cancel() - tasks[taskKey] = nil - } - - private func finish(with error: Error?, taskKey: ObjectIdentifier) { - guard let task = tasks[taskKey] else { return } - - if let error { - task.webViewTask.didFailWithError(error) - } else { - task.webViewTask.didFinish() - } - tasks[taskKey] = nil - } - } -}