Wesley de Groot's Blog
Creating a dynamic user interface for extensions in Aurora Editor

Back

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.

  1. Create a struct for the Dynamic UI Builder (Element definition in the JSON).
  2. Create a view for the Dynamic UI Element (SwiftUI view for the element).
  3. Create a DynamicUI? (Display the UI).
  4. Create a DynamicUI. (Convert JSON to SwiftUI view).
  5. Use the DynamicUI.
  6. 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

Share


Share Mastodon Twitter LinkedIn Facebook
x-twitter mastodon github linkedin discord threads instagram whatsapp bluesky square-rss sitemap