DEV Community

Wesley de Groot
Wesley de Groot

Posted on • Originally published at wesleydegroot.nl on

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()
    ]
)
Enter fullscreen mode Exit fullscreen mode

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>",
});
Enter fullscreen mode Exit fullscreen mode

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?
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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 }
    }
}
Enter fullscreen mode Exit fullscreen mode

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))
    }
}
Enter fullscreen mode Exit fullscreen mode

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!"
                }
            ]
            """
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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!\"}]"
});
Enter fullscreen mode Exit fullscreen mode

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!"
      }]
    }
);
Enter fullscreen mode Exit fullscreen mode

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:

AWS Security LIVE! Stream

Stream AWS Security LIVE!

See how AWS is redefining security by design with simple, seamless solutions on Security LIVE!

Learn More

Top comments (0)

Heroku

Amplify your impact where it matters most — building exceptional apps.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

👋 Kindness is contagious

Value this insightful article and join the thriving DEV Community. Developers of every skill level are encouraged to contribute and expand our collective knowledge.

A simple “thank you” can uplift someone’s spirits. Leave your appreciation in the comments!

On DEV, exchanging expertise lightens our path and reinforces our bonds. Enjoyed the read? A quick note of thanks to the author means a lot.

Okay