Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions ios/Demo-iOS/Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ let editorURL: URL? = ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_URL"

struct ContentView: View {

@AppStorage("UseSwiftUIView") var useSwiftUI: Bool = false

let remoteEditorConfigurations: [EditorConfiguration] = [.template]

var body: some View {
List {
Section {
NavigationLink {
EditorView(configuration: .default)
EditorView(configuration: .default, useSwiftUI: useSwiftUI)
} label: {
Text("Bundled Editor")
}
Expand All @@ -20,7 +22,7 @@ struct ContentView: View {
Section {
ForEach(remoteEditorConfigurations, id: \.siteURL) { configuration in
NavigationLink {
EditorView(configuration: configuration)
EditorView(configuration: configuration, useSwiftUI: useSwiftUI)
} label: {
Text(URL(string: configuration.siteURL)?.host ?? configuration.siteURL)
}
Expand All @@ -38,6 +40,10 @@ struct ContentView: View {
Text("Note: The editor is backed by the compiled web app created by `make build`.")
}
}

Section("Configuration") {
Toggle(isOn: $useSwiftUI) { Text("Use SwiftUI WebView") }
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Expand All @@ -57,7 +63,6 @@ struct ContentView: View {
} label: {
Image(systemName: "arrow.clockwise")
}

}
}
}
Expand Down
24 changes: 17 additions & 7 deletions ios/Demo-iOS/Sources/EditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ import SwiftUI
import GutenbergKit

struct EditorView: View {
private let configuration: EditorConfiguration

init(configuration: EditorConfiguration) {
self.configuration = configuration
}
let configuration: EditorConfiguration
let useSwiftUI: Bool

var body: some View {
_EditorView(configuration: configuration)
editorView
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
Button(action: {}, label: {
Expand All @@ -35,6 +32,19 @@ struct EditorView: View {
}
}

@ViewBuilder
var editorView: some View {
if #available(iOS 26.0, *) {
if useSwiftUI {
GutenbergEditor(configuration: configuration)
} else {
_EditorView(configuration: configuration)
}
} else {
_EditorView(configuration: configuration)
}
}

private var moreMenu: some View {
Menu {
Section {
Expand Down Expand Up @@ -96,6 +106,6 @@ private struct _EditorView: UIViewControllerRepresentable {

#Preview {
NavigationStack {
EditorView(configuration: .default)
EditorView(configuration: .default, useSwiftUI: false)
}
}
123 changes: 78 additions & 45 deletions ios/Sources/GutenbergKit/Sources/Cache/CachedAssetSchemeHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class CachedAssetSchemeHandler: NSObject, WKURLSchemeHandler {
return nil
}

let worker: Worker
private let worker: Worker

init(library: EditorAssetsLibrary) {
self.worker = .init(library: library)
Expand All @@ -40,69 +40,102 @@ class CachedAssetSchemeHandler: NSObject, WKURLSchemeHandler {
await worker.stop(urlSchemeTask)
}
}
}

actor Worker {
struct TaskInfo {
var webViewTask: WKURLSchemeTask
var fetchAssetTask: Task<Void, Never>
private actor Worker {
struct TaskInfo {
var webViewTask: WKURLSchemeTask
var fetchAssetTask: Task<Void, Never>

func cancel() {
fetchAssetTask.cancel()
}
}
func cancel() {
fetchAssetTask.cancel()
}
}

let library: EditorAssetsLibrary
var tasks: [ObjectIdentifier: TaskInfo] = [:]

init(library: EditorAssetsLibrary) {
self.library = library
}

let library: EditorAssetsLibrary
var tasks: [ObjectIdentifier: TaskInfo] = [:]
deinit {
for (_, task) in tasks {
task.cancel()
}
}

init(library: EditorAssetsLibrary) {
self.library = library
func start(_ task: WKURLSchemeTask) {
guard let url = task.request.url, let httpURL = CachedAssetSchemeHandler.originalHTTPURL(from: url) else {
task.didFailWithError(URLError(.badURL))
return
}

deinit {
for (_, task) in tasks {
task.cancel()
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 start(_ task: WKURLSchemeTask) {
guard let url = task.request.url, let httpURL = CachedAssetSchemeHandler.originalHTTPURL(from: url) else {
task.didFailWithError(URLError(.badURL))
return
}
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 }

let taskKey = ObjectIdentifier(task)
if let error {
task.webViewTask.didFailWithError(error)
} else {
task.webViewTask.didFinish()
}
tasks[taskKey] = nil
}
}

@available(iOS 26.0, *)
extension CachedAssetSchemeHandler: URLSchemeHandler {
func reply(for request: URLRequest) -> AsyncThrowingStream<URLSchemeTaskResult, Error> {
AsyncThrowingStream { [library = worker.library] continuation in
let task = Task {
guard let url = request.url,
let httpURL = CachedAssetSchemeHandler.originalHTTPURL(from: url) else {
continuation.yield(with: .failure(URLError(.badURL)))
continuation.finish()
return
}

let fetchAssetTask = Task { [library, weak self] in
do {
let (response, content) = try await library.cacheAsset(from: httpURL, webViewURL: url)
try Task.checkCancellation()

await self?.tasks[taskKey]?.webViewTask.didReceive(response)
await self?.tasks[taskKey]?.webViewTask.didReceive(content)

await self?.finish(with: nil, taskKey: taskKey)
continuation.yield(with: .success(.response(response)))
continuation.yield(with: .success(.data(content)))
} catch {
await self?.finish(with: error, taskKey: taskKey)
try Task.checkCancellation()
continuation.yield(with: .failure(error))
}
continuation.finish()
}
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()
continuation.onTermination = {
if case .cancelled = $0 {
task.cancel()
}
}
tasks[taskKey] = nil
}
}
}

65 changes: 65 additions & 0 deletions ios/Sources/GutenbergKit/Sources/SwiftUI/GutenbergEditor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Foundation
import SwiftUI
import WebKit
import Combine

@available(iOS 26.0, *)
public struct GutenbergEditor: View {
@StateObject private var viewModel: GutenbergEditorViewModel

public init(configuration: EditorConfiguration = .default) {
self._viewModel = StateObject(wrappedValue: GutenbergEditorViewModel(configuration: configuration))
}

public var body: some View {
WebView(viewModel.webPage)
.textSelection(.enabled)
.scrollDismissesKeyboard(.interactively)
.task {
await viewModel.loadEditor()
}
}

}

@available(iOS 26.0, *)
@MainActor
private final class GutenbergEditorViewModel: ObservableObject {
@Published private(set) var webPage: WebPage

var configuration: EditorConfiguration
private let webBridge: WebBridge
private let controller: GutenbergEditorController

init(configuration: EditorConfiguration = .default) {
self.configuration = configuration
self.webBridge = WebBridge(configuration: configuration)
self.controller = GutenbergEditorController(configuration: configuration)

var config = WebPage.Configuration()
webBridge.configure(with: &config)

self.webPage = WebPage(configuration: config, navigationDecider: controller)

#if DEBUG
self.webPage.isInspectable = true
#endif
}

func loadEditor() async {
if configuration.plugins {
// Handle remote editor loading
if let remoteURL = ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_REMOTE_URL"].flatMap(URL.init) {
webPage.load(URLRequest(url: remoteURL))
} else {
let remoteURL = Bundle.module.url(forResource: "remote", withExtension: "html", subdirectory: "Gutenberg")!
webPage.load(URLRequest(url: remoteURL))
}
} else if let editorURL = ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_URL"].flatMap(URL.init) {
webPage.load(URLRequest(url: editorURL))
} else {
let indexURL = Bundle.module.url(forResource: "index", withExtension: "html", subdirectory: "Gutenberg")!
webPage.load(URLRequest(url: indexURL))
}
}
}
Loading