Creating a dynamic user interface for extensions in Aurora Editor
As some of you may know, I have been working on a IDE called Aurora Editor.
It is a editor that is built using Swift and SwiftUI and is designed to be extensible and native.
In this post, I will go trough the process I went to create a dynamic user interface for extensions in Aurora Editor.
This is an (my first) in-depth post, let's get started!
I hope it will be helpful for you, and i'd love to get feedback on this post.
The problem
It all started with a problem, how can a extension developer create a user interface for their extensions in Aurora Editor?
I first started with a simple solution, using a SwiftUI view, from a Swift extension.
import SwiftUI
struct ExtensionView: View {
var body: some View {
Text("Hello, World!")
}
}
// This is the view that will be displayed trough a extension window
ExtensionsManager.shared.sendEvent(
event: "openWindow",
parameters: [
"view": ExtensionView()
]
)
This works great, but it does not allow views from a JavaScript extension to be displayed.
I needed a way to create a dynamic user interface that could be used by both native (Swift) and JavaScript extensions.
I first started with a WebView that would display the user interface, from a HTML string in a JavaScript extension, this is a working example from Aurora Editor.
// This is the view that will be displayed trough a extension window
AuroraEditor.respond("openWindow", {
view: "<html><body><h1>Hello, World!</h1></body></html>",
});
This works fine, but it is not really native and it might not be the best solution because we want a (near) native experience.
What if I can use ... what can be used to create a dynamic user interface?
- SwiftUI: Unfortunately, SwiftUI cannot be shared between JavaScript and Swift
- JavaScript: No this would mean we need to create a custom and difficult to maintain bridge between JavaScript and SwiftUI
- JSON: This is a good idea, it is easy to parse and can? be used to create a dynamic user interface, let's try this!
The birth of DynamicUI
So JSON it is! I created a new struct called DynamicUI
that can be used to create a dynamic user interface.
We need to create a way to convert JSON to a SwiftUI view.
We can probably achive this by following these steps:
- Read the JSON (Data or String).
- Parse the JSON.
- Create a SwiftUI view from the parsed JSON.
- Return the SwiftUI view.
- Done!
Sounds easy right? Let's try it!
We can't start with the list of steps above, we need to do them in a slightly different order.
- Create a struct for the Dynamic UI Builder (Element definition in the JSON).
- Create a view for the Dynamic UI Element (SwiftUI view for the element).
- Create a DynamicUI? (Display the UI).
- Create a DynamicUI. (Convert JSON to SwiftUI view).
- Use the DynamicUI.
- Use the DynamicUI in Aurora Editor.
Step 1: Creating a struct for the Dynamic UI Element
We need to create a struct that can be used to create a dynamic user interface element.
import SwiftUI
public struct DynamicUIComponent: Codable, Hashable {
public let type: String
public let title: String?
public let text: String?
public let defaultValue: AnyCodable?
}
Hey, what is AnyCodable
?
AnyCodeable is a type-erased codable value, it can be used to store any codable value (e.g. String
, Int
, Bool
, ...).
We have a struct that can be used to create a dynamic user interface element. Full source code
Step 2: Create a Dynamic UI Element
We now need to create a view that can be used to create a dynamic user interface element.
We create a new struct so we can pass all parameters required, and can append modifiers to the view.
In this example, we will create a DynamicText
element (to display text).
I left out the .dynamicUIModifiers()
(modifier parser) code, since this will make the sample code to complex.
import SwiftUI
public struct DynamicText: View {
/// The component to display
private let component: DynamicUIComponent
/// Initialize the DynamicText
init(_ component: DynamicUIComponent) {
self.component = component
}
/// Generated body for SwiftUI
public var body: some View {
Text(.init(component.title ?? ""))
.dynamicUIModifiers(component.modifiers)
}
}
Step 3: Create a DynamicUI?
Can we already create a dynamic user interface element using the DynamicUIComponent
and DynamicText
or do we need to create more? Unfortunately, we need to create more.
We need to create a way to convert to parse the JSON and type-erease it.
We create a new struct called InternalDynamicUI
that can be used to create a dynamic user interface.
struct InternalDynamicUI: View {
/// JSON Data
public var json: Data?
@State
/// This state is used to store the layout
private var layout: [DynamicUIComponent]?
@State
/// This state is used to store the error message
private var error: String?
/// Initialize the InternalDynamicUI
var body: some View {
VStack {
if let layout = layout {
buildView(for: layout)
} else if let error = error {
Text(error)
} else {
ProgressView()
.frame(width: 150, height: 150)
.padding()
Text("Generating interface...")
}
}
.onAppear {
decodeJSON()
}
}
/// Decode the JSON data
private func decodeJSON() {
do {
if let json = json {
self.layout = try JSONDecoder().decode(
[DynamicUIComponent].self,
from: json
)
}
} catch {
self.error = "Error decoding JSON: \(error)"
}
}
/// Build a SwiftUI View based on the components
/// - Parameter components: [UIComponent]
/// - Returns: A SwiftUI View
func buildView(for components: [DynamicUIComponent]) -> some View {
return ForEach(components, id: \.self) { component in
switch component.type {
case "Text":
DynamicText(component)
.environment(\.internalDynamicUIEnvironment, self)
default:
EmptyView()
}
}
}
}
private struct InternalDynamicUIKey: EnvironmentKey {
static let defaultValue: InternalDynamicUI = defaultValue
}
extension EnvironmentValues {
var internalDynamicUIEnvironment: InternalDynamicUI {
get { self[InternalDynamicUIKey.self] }
set { self[InternalDynamicUIKey.self] = newValue }
}
}
Step 4: Create a DynamicUI
We have now created all components needed to create our dynamic user interface.
We can now create a new struct called DynamicUI
that can be used to create a dynamic user interface.
We create 2 initializers for the DynamicUI
struct, one for JSON Data and one for JSON String.
public struct DynamicUI: View {
/// DynamicUIComponent state change handler
public typealias Handler = (DynamicUIComponent) -> Void
/// JSON data
public var json: Data?
/// Initialize DynamicUI
///
/// - Parameter json: JSON Data
public init(json: Data? = nil) {
self.json = json
}
/// Initialize DynamicUI
///
/// - Parameter json: JSON String
public init(json: String? = nil) {
// Convert the JSON string to data
self.json = json?.data(using: .utf8)
}
/// Generated body for SwiftUI
public var body: some View {
AnyView(InternalDynamicUI(json: json))
}
}
Step 5: Use the DynamicUI
We can now use the DynamicUI
to create a dynamic user interface.
import SwiftUI
struct ContentView: View {
var body: some View {
DynamicUI(
json: """
[
{
"type": "Text",
"title": "Hello, World!"
}
]
"""
)
}
}
Step 6: Use the DynamicUI in Aurora Editor
We can now use the DynamicUI
in Aurora Editor to create a dynamic user interface for extensions.
We can pass the JSON string to the DynamicUI
and display the user interface.
// This is the view that will be displayed trough a extension window
AuroraEditor.respond("openWindow", {
view: "[{\"type\":\"Text\",\"title\":\"Hello, World!\"}]"
});
We can also pass the JSON as a object to the DynamicUI
and display the user interface.
// This is the view that will be displayed trough a extension window
AuroraEditor.respond(
"openWindow",
{
view: [{
"type": "Text",
"title": "Hello, World!"
}]
}
);
Playground
I have created a playground app that can be used to test the DynamicUI
framework.
Conclusion
Creating a dynamic user interface for extensions in Aurora Editor was a challenge, but it is really fun, it need some more improvements but the basics are there.
I think i can improve some parts to make more use of @ViewBuilder.
Resources:
Read more
- Simplifying multiplatform Colors • 15 minutes reading time.
- SwiftUI ViewModifiers • 6 minutes reading time.
- Translating closures to async • 4 minutes reading time.
Share
Share Mastodon Twitter LinkedIn Facebook