Skip to content

Commit eb2fdba

Browse files
committed
Extract WebBridge to be reused in SwiftUI.WebView
1 parent 7af1d8e commit eb2fdba

File tree

8 files changed

+323
-143
lines changed

8 files changed

+323
-143
lines changed

ios/Demo-iOS/Sources/ContentView.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ let editorURL: URL? = ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_URL"
55

66
struct ContentView: View {
77

8+
@AppStorage("UseSwiftUIView") var useSwiftUI: Bool = false
9+
810
let remoteEditorConfigurations: [EditorConfiguration] = [.template]
911

1012
var body: some View {
1113
List {
1214
Section {
1315
NavigationLink {
14-
EditorView(configuration: .default)
16+
EditorView(configuration: .default, useSwiftUI: useSwiftUI)
1517
} label: {
1618
Text("Bundled Editor")
1719
}
@@ -20,7 +22,7 @@ struct ContentView: View {
2022
Section {
2123
ForEach(remoteEditorConfigurations, id: \.siteURL) { configuration in
2224
NavigationLink {
23-
EditorView(configuration: configuration)
25+
EditorView(configuration: configuration, useSwiftUI: useSwiftUI)
2426
} label: {
2527
Text(URL(string: configuration.siteURL)?.host ?? configuration.siteURL)
2628
}
@@ -38,6 +40,10 @@ struct ContentView: View {
3840
Text("Note: The editor is backed by the compiled web app created by `make build`.")
3941
}
4042
}
43+
44+
Section("Configuration") {
45+
Toggle(isOn: $useSwiftUI) { Text("Use SwiftUI WebView") }
46+
}
4147
}
4248
.toolbar {
4349
ToolbarItem(placement: .primaryAction) {
@@ -57,7 +63,6 @@ struct ContentView: View {
5763
} label: {
5864
Image(systemName: "arrow.clockwise")
5965
}
60-
6166
}
6267
}
6368
}

ios/Demo-iOS/Sources/EditorView.swift

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@ import SwiftUI
22
import GutenbergKit
33

44
struct EditorView: View {
5-
private let configuration: EditorConfiguration
6-
7-
init(configuration: EditorConfiguration) {
8-
self.configuration = configuration
9-
}
5+
let configuration: EditorConfiguration
6+
let useSwiftUI: Bool
107

118
var body: some View {
12-
_EditorView(configuration: configuration)
9+
editorView
1310
.toolbar {
1411
ToolbarItemGroup(placement: .topBarLeading) {
1512
Button(action: {}, label: {
@@ -35,6 +32,19 @@ struct EditorView: View {
3532
}
3633
}
3734

35+
@ViewBuilder
36+
var editorView: some View {
37+
if #available(iOS 26.0, *) {
38+
if useSwiftUI {
39+
GutenbergEditor(configuration: configuration)
40+
} else {
41+
_EditorView(configuration: configuration)
42+
}
43+
} else {
44+
_EditorView(configuration: configuration)
45+
}
46+
}
47+
3848
private var moreMenu: some View {
3949
Menu {
4050
Section {
@@ -96,6 +106,6 @@ private struct _EditorView: UIViewControllerRepresentable {
96106

97107
#Preview {
98108
NavigationStack {
99-
EditorView(configuration: .default)
109+
EditorView(configuration: .default, useSwiftUI: false)
100110
}
101111
}

ios/Sources/GutenbergKit/Sources/Cache/CachedAssetSchemeHandler.swift

Lines changed: 78 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class CachedAssetSchemeHandler: NSObject, WKURLSchemeHandler {
2323
return nil
2424
}
2525

26-
let worker: Worker
26+
private let worker: Worker
2727

2828
init(library: EditorAssetsLibrary) {
2929
self.worker = .init(library: library)
@@ -40,69 +40,102 @@ class CachedAssetSchemeHandler: NSObject, WKURLSchemeHandler {
4040
await worker.stop(urlSchemeTask)
4141
}
4242
}
43+
}
4344

44-
actor Worker {
45-
struct TaskInfo {
46-
var webViewTask: WKURLSchemeTask
47-
var fetchAssetTask: Task<Void, Never>
45+
private actor Worker {
46+
struct TaskInfo {
47+
var webViewTask: WKURLSchemeTask
48+
var fetchAssetTask: Task<Void, Never>
4849

49-
func cancel() {
50-
fetchAssetTask.cancel()
51-
}
52-
}
50+
func cancel() {
51+
fetchAssetTask.cancel()
52+
}
53+
}
54+
55+
let library: EditorAssetsLibrary
56+
var tasks: [ObjectIdentifier: TaskInfo] = [:]
57+
58+
init(library: EditorAssetsLibrary) {
59+
self.library = library
60+
}
5361

54-
let library: EditorAssetsLibrary
55-
var tasks: [ObjectIdentifier: TaskInfo] = [:]
62+
deinit {
63+
for (_, task) in tasks {
64+
task.cancel()
65+
}
66+
}
5667

57-
init(library: EditorAssetsLibrary) {
58-
self.library = library
68+
func start(_ task: WKURLSchemeTask) {
69+
guard let url = task.request.url, let httpURL = CachedAssetSchemeHandler.originalHTTPURL(from: url) else {
70+
task.didFailWithError(URLError(.badURL))
71+
return
5972
}
6073

61-
deinit {
62-
for (_, task) in tasks {
63-
task.cancel()
74+
let taskKey = ObjectIdentifier(task)
75+
76+
let fetchAssetTask = Task { [library, weak self] in
77+
do {
78+
let (response, content) = try await library.cacheAsset(from: httpURL, webViewURL: url)
79+
80+
await self?.tasks[taskKey]?.webViewTask.didReceive(response)
81+
await self?.tasks[taskKey]?.webViewTask.didReceive(content)
82+
83+
await self?.finish(with: nil, taskKey: taskKey)
84+
} catch {
85+
await self?.finish(with: error, taskKey: taskKey)
6486
}
6587
}
88+
tasks[taskKey] = .init(webViewTask: task, fetchAssetTask: fetchAssetTask)
89+
}
6690

67-
func start(_ task: WKURLSchemeTask) {
68-
guard let url = task.request.url, let httpURL = CachedAssetSchemeHandler.originalHTTPURL(from: url) else {
69-
task.didFailWithError(URLError(.badURL))
70-
return
71-
}
91+
func stop(_ task: WKURLSchemeTask) {
92+
let taskKey = ObjectIdentifier(task)
93+
tasks[taskKey]?.cancel()
94+
tasks[taskKey] = nil
95+
}
96+
97+
private func finish(with error: Error?, taskKey: ObjectIdentifier) {
98+
guard let task = tasks[taskKey] else { return }
7299

73-
let taskKey = ObjectIdentifier(task)
100+
if let error {
101+
task.webViewTask.didFailWithError(error)
102+
} else {
103+
task.webViewTask.didFinish()
104+
}
105+
tasks[taskKey] = nil
106+
}
107+
}
108+
109+
@available(iOS 26.0, *)
110+
extension CachedAssetSchemeHandler: URLSchemeHandler {
111+
func reply(for request: URLRequest) -> AsyncThrowingStream<URLSchemeTaskResult, Error> {
112+
AsyncThrowingStream { [library = worker.library] continuation in
113+
let task = Task {
114+
guard let url = request.url,
115+
let httpURL = CachedAssetSchemeHandler.originalHTTPURL(from: url) else {
116+
continuation.yield(with: .failure(URLError(.badURL)))
117+
continuation.finish()
118+
return
119+
}
74120

75-
let fetchAssetTask = Task { [library, weak self] in
76121
do {
77122
let (response, content) = try await library.cacheAsset(from: httpURL, webViewURL: url)
123+
try Task.checkCancellation()
78124

79-
await self?.tasks[taskKey]?.webViewTask.didReceive(response)
80-
await self?.tasks[taskKey]?.webViewTask.didReceive(content)
81-
82-
await self?.finish(with: nil, taskKey: taskKey)
125+
continuation.yield(with: .success(.response(response)))
126+
continuation.yield(with: .success(.data(content)))
83127
} catch {
84-
await self?.finish(with: error, taskKey: taskKey)
128+
try Task.checkCancellation()
129+
continuation.yield(with: .failure(error))
85130
}
131+
continuation.finish()
86132
}
87-
tasks[taskKey] = .init(webViewTask: task, fetchAssetTask: fetchAssetTask)
88-
}
89-
90-
func stop(_ task: WKURLSchemeTask) {
91-
let taskKey = ObjectIdentifier(task)
92-
tasks[taskKey]?.cancel()
93-
tasks[taskKey] = nil
94-
}
95133

96-
private func finish(with error: Error?, taskKey: ObjectIdentifier) {
97-
guard let task = tasks[taskKey] else { return }
98-
99-
if let error {
100-
task.webViewTask.didFailWithError(error)
101-
} else {
102-
task.webViewTask.didFinish()
134+
continuation.onTermination = {
135+
if case .cancelled = $0 {
136+
task.cancel()
137+
}
103138
}
104-
tasks[taskKey] = nil
105139
}
106140
}
107141
}
108-
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Foundation
2+
import SwiftUI
3+
import WebKit
4+
import Combine
5+
6+
@available(iOS 26.0, *)
7+
public struct GutenbergEditor: View {
8+
@StateObject private var viewModel: GutenbergEditorViewModel
9+
10+
public init(configuration: EditorConfiguration = .default) {
11+
self._viewModel = StateObject(wrappedValue: GutenbergEditorViewModel(configuration: configuration))
12+
}
13+
14+
public var body: some View {
15+
WebView(viewModel.webPage)
16+
.textSelection(.enabled)
17+
.scrollDismissesKeyboard(.interactively)
18+
.task {
19+
await viewModel.loadEditor()
20+
}
21+
}
22+
23+
}
24+
25+
@available(iOS 26.0, *)
26+
@MainActor
27+
private final class GutenbergEditorViewModel: ObservableObject {
28+
@Published private(set) var webPage: WebPage
29+
30+
var configuration: EditorConfiguration
31+
private let webBridge: WebBridge
32+
private let controller: GutenbergEditorController
33+
34+
init(configuration: EditorConfiguration = .default) {
35+
self.configuration = configuration
36+
self.webBridge = WebBridge(configuration: configuration)
37+
self.controller = GutenbergEditorController(configuration: configuration)
38+
39+
var config = WebPage.Configuration()
40+
webBridge.configure(with: &config)
41+
42+
self.webPage = WebPage(configuration: config, navigationDecider: controller)
43+
44+
#if DEBUG
45+
self.webPage.isInspectable = true
46+
#endif
47+
}
48+
49+
func loadEditor() async {
50+
if configuration.plugins {
51+
// Handle remote editor loading
52+
if let remoteURL = ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_REMOTE_URL"].flatMap(URL.init) {
53+
webPage.load(URLRequest(url: remoteURL))
54+
} else {
55+
let remoteURL = Bundle.module.url(forResource: "remote", withExtension: "html", subdirectory: "Gutenberg")!
56+
webPage.load(URLRequest(url: remoteURL))
57+
}
58+
} else if let editorURL = ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_URL"].flatMap(URL.init) {
59+
webPage.load(URLRequest(url: editorURL))
60+
} else {
61+
let indexURL = Bundle.module.url(forResource: "index", withExtension: "html", subdirectory: "Gutenberg")!
62+
webPage.load(URLRequest(url: indexURL))
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)