Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add iOS support for custom bundles #107

Draft
wants to merge 2 commits into
base: trunk
Choose a base branch
from
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
7 changes: 5 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ let package = Package(
name: "GutenbergKitTests",
dependencies: ["GutenbergKit"],
path: "ios/Tests",
exclude: []
)
exclude: [],
resources: [
.copy("GutenbergKitTests/Resources/manifest-test-case-1.json")
]
),
]
)
4 changes: 4 additions & 0 deletions ios/Demo-iOS/Gutenberg.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
0CE8E78E2C339B0600B9DC67 /* GutenbergApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE8E7872C339B0600B9DC67 /* GutenbergApp.swift */; };
0CE8E78F2C339B0600B9DC67 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CE8E7892C339B0600B9DC67 /* Preview Assets.xcassets */; };
0CF6E04C2BEFF60E00EDEE8A /* GutenbergKit in Frameworks */ = {isa = PBXBuildFile; productRef = 0CF6E04B2BEFF60E00EDEE8A /* GutenbergKit */; };
2441AFD92D77E5DA00563F80 /* EditorDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2441AFD82D77E5DA00563F80 /* EditorDownloadView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -23,6 +24,7 @@
0CE8E7872C339B0600B9DC67 /* GutenbergApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GutenbergApp.swift; sourceTree = "<group>"; };
0CE8E7892C339B0600B9DC67 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
0CE8E7922C339B1B00B9DC67 /* GutenbergKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = GutenbergKit; path = ../..; sourceTree = "<group>"; };
2441AFD82D77E5DA00563F80 /* EditorDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorDownloadView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -69,6 +71,7 @@
isa = PBXGroup;
children = (
0CE8E7852C339B0600B9DC67 /* ContentView.swift */,
2441AFD82D77E5DA00563F80 /* EditorDownloadView.swift */,
0CE8E7862C339B0600B9DC67 /* EditorView.swift */,
0CE8E7872C339B0600B9DC67 /* GutenbergApp.swift */,
);
Expand Down Expand Up @@ -164,6 +167,7 @@
buildActionMask = 2147483647;
files = (
0CE8E78E2C339B0600B9DC67 /* GutenbergApp.swift in Sources */,
2441AFD92D77E5DA00563F80 /* EditorDownloadView.swift in Sources */,
0CE8E78C2C339B0600B9DC67 /* ContentView.swift in Sources */,
0CE8E78D2C339B0600B9DC67 /* EditorView.swift in Sources */,
);
Expand Down
87 changes: 86 additions & 1 deletion ios/Demo-iOS/Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,96 @@ import GutenbergKit

let editorURL: URL? = ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_URL"].flatMap(URL.init)

@Observable
class EditorListViewModel {

var manifests: [LocalEditorManifest] = []

var hasError: Bool = false

var error: Error? = nil

func load() async {
do {
self.manifests = try await EditorLibrary.shared.listManifests()
} catch {
self.error = error
}
}

func remove(atOffsets offsets: IndexSet) {
Task {
do {
for offset in offsets {
let manifestToRemove = manifests[offset]
try await EditorLibrary.shared.remove(manifest: manifestToRemove)
}
} catch {
self.error = error
}

await self.load()
}
}
}

struct ContentView: View {
@State
private var viewModel = EditorListViewModel()

let bundledManifest = EditorLibrary.shared.bundledManifest

var body: some View {
NavigationView {
EditorView(editorURL: editorURL)
List {
if let error = viewModel.error {
Text("Error fetching manifests: \(error.localizedDescription)")
}

Section {
NavigationLink(value: bundledManifest) {
Text("Bundled Gutenberg")
}
}

if !viewModel.manifests.isEmpty {
Section("Downloaded Bundles") {
ForEach(viewModel.manifests) { manifest in
NavigationLink(value: manifest) {
Text(manifest.name)
}
}.onDelete(perform: deleteManifests)
}
}
}
}
.task {
await self.viewModel.load()
}
.navigationDestination(for: LocalEditorManifest.self) { manifest in
EditorView(editorManifest: manifest)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
NavigationLink("Add Editor") {
EditorDownloadView()
}
}
}
}

func deleteManifests(_ offsets: IndexSet) {
viewModel.remove(atOffsets: offsets)
}
}

extension LocalEditorManifest: @retroactive Identifiable {
public var id: String {
self.rootDirectory.path
}

var name: String {
self.rootDirectory.deletingPathExtension().lastPathComponent
}
}

Expand Down
67 changes: 67 additions & 0 deletions ios/Demo-iOS/Sources/EditorDownloadView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import SwiftUI
import GutenbergKit

struct EditorDownloadView: View {

class ViewModel: ObservableObject {
@Published
var downloadProgress: Double = 0

@Published
var siteUrl: String = "http://localhost"

@Published
var error: Error? = nil

func download() {
Task {

let url = URL(string: siteUrl)!
.appendingPathComponent("wp-json")
.appendingPathComponent("__experimental")
.appendingPathComponent("wp-block-editor")
.appendingPathComponent("v1")
.appendingPathComponent( "editor-assets" )
Comment on lines +19 to +24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify using different endpoints—e.g., it varies based on environment—we might use this path as a default if a full URL is not provided.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can/should be improved a bunch. Because it's the demo app, I just whipped up something quick.

In the real world, we'd probably use https://github.com/Automattic/wordpress-rs to find this endpoint in a programmatic way by scanning the wp-json endpoint for its entry. If it's not there, we'd assume the site doesn't support editor assets and default back to the bundled editor.

I think ... LMK if this doesn't seem like a good plan?


print("Downloading from \(url)")

do {
self.error = nil

try await EditorLibrary.shared.downloadManifest(from: url) { progress in
self.downloadProgress = Double(progress.fractionCompleted)
}
} catch {
self.error = error
}
}
}
}

@StateObject
var viewModel = ViewModel()

var body: some View {
Form {
if let error = viewModel.error {
Text("Error: \(error.localizedDescription)")
}

TextField(text: $viewModel.siteUrl, prompt: Text("Site URL")) {
Text("Site URL")
}
.keyboardType(.URL)
.autocapitalization(.none)

Button("Download") {
self.viewModel.download()
}

ProgressView(value: viewModel.downloadProgress).progressViewStyle(.linear)
}
}
}

#Preview {
EditorDownloadView()
}
17 changes: 12 additions & 5 deletions ios/Demo-iOS/Sources/EditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import SwiftUI
import GutenbergKit

struct EditorView: View {
var editorURL: URL?
private let editorManifest: LocalEditorManifest

init(editorManifest: LocalEditorManifest = EditorLibrary.shared.bundledManifest) {
self.editorManifest = editorManifest
}

var body: some View {
_EditorView(editorURL: editorURL)
_EditorView(editorManifest: editorManifest)
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
Button(action: {}, label: {
Expand Down Expand Up @@ -69,11 +73,14 @@ struct EditorView: View {
}

private struct _EditorView: UIViewControllerRepresentable {
var editorURL: URL?
let editorManifest: LocalEditorManifest

func makeUIViewController(context: Context) -> EditorViewController {
let viewController = EditorViewController()
viewController.editorURL = editorURL
let viewController = EditorViewController(
manifest: editorManifest,
editorLibrary: EditorLibrary.shared
)

if #available(iOS 16.4, *) {
viewController.webView.isInspectable = true
}
Expand Down
10 changes: 9 additions & 1 deletion ios/Demo-iOS/Sources/GutenbergApp.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import SwiftUI
import GutenbergKit

@main
struct GutenbergApp: App {
var body: some Scene {
WindowGroup {
ContentView()
NavigationStack {
ContentView()
}
}
}
}

// We don't really care about dependency injection for our demo app, so we'll just make a bunch of singletons
extension EditorLibrary {
static let shared = EditorLibrary()
}
8 changes: 8 additions & 0 deletions ios/Sources/GutenbergKit/Sources/EditorConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ public struct EditorConfiguration {
self.title = title
self.content = content
}

var manifestURL: URL {
URL(string: siteApiRoot)!
.appendingPathComponent("__experimental")
.appendingPathComponent("wp-block-editor")
.appendingPathComponent("v1")
.appendingPathComponent( "editor-assets" )
Comment on lines +25 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify using different endpoints—e.g., it varies based on environment—we might use this path as a default if a full URL is not provided.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be far better to inject this, I think.

We already have it in the demo app, so I think it'd just be a matter of passing it in here?

}
}
Loading