Contents

0xwdg/dynamicui

Render server-driven SwiftUI interfaces from JSON across Apple platforms.

Why DynamicUI?

  • Native SwiftUI views on iOS, macOS, tvOS, watchOS, Mac Catalyst, and visionOS.
  • Nested layouts, interactive controls, conditional content, and runtime value updates.
  • Versioned schemas with validation before rendering.
  • Application-defined components through a custom renderer.
  • Explicit VoiceOver, Voice Control, and UI-test metadata in JSON.

Requirements

  • Swift 5.9+ (Xcode 15+)
  • iOS 15+, macOS 12+, tvOS 14+, watchOS 8+, Mac Catalyst 15+, visionOS 1.0+

Installation

Add DynamicUI using Swift Package Manager:

dependencies: [
    .package(url: "https://github.com/0xWDG/DynamicUI.git", exact: "0.1.1"),
],
targets: [
    .target(name: "MyTarget", dependencies: [
        .product(name: "DynamicUI", package: "DynamicUI"),
    ]),
]

In Xcode, select File β†’ Add Package Dependencies, enter https://github.com/0xWDG/DynamicUI, and choose version 0.1.1.

Then import the package:

import DynamicUI

Quick start

import SwiftUI
import DynamicUI

struct ContentView: View {
    let json = """
    [
        {
            "type": "Text",
            "title": "This interface comes from JSON",
            "modifiers": {"foregroundStyle":"red","opacity":0.6}
        },
        {
            "type": "Button",
            "title": "Continue",
            "eventHandler": "continue"
        },
        {
            "type": "Toggle",
            "title": "Show details",
            "identifier": "showDetails"
        }
    ]
    """

    @State private var component: DynamicUIComponent?
    @State private var error: Error?

    var body: some View {
        DynamicUI(json: json, component: $component, error: $error)
    }
}

Versioned layouts and validation

For production payloads, use the versioned layout envelope:

{
    "schemaVersion": 1,
    "components": [
        { "type": "Text", "title": "A validated layout" }
    ]
}

Legacy top-level component arrays remain supported. Validate either representation before rendering with try DynamicUILayout(json: json). Validation rejects unsupported schema versions, empty types and identifiers, duplicate identifiers, and malformed component conditions.

Conditional content

Conditional expressions can select strings or control whether an entire component renders:

[
    {
        "type": "Toggle",
        "title": "Show favorite",
        "identifier": "favorite"
    },
    {
        "type": "Label",
        "title": "{$favorite ? Saved : Not saved}",
        "url": "{$favorite ? star.fill : star}"
    },
    {
        "type": "Text",
        "title": "The favorite is enabled",
        "if": "$favorite"
    }
]

The string syntax is {$identifier ? valueWhenTrue : valueWhenFalse}. Missing identifiers and empty, zero, false, or null values select the false branch and hide conditional views.

Handle interactions

The component binding receives the latest interacted-with component. Stateful controls include their new value in state; identifier and eventHandler let your application route the update.

.onChange(of: component) { component in
    guard let component else { return }

    print(component.identifier as Any)
    print(component.eventHandler as Any)
    print(component.state as Any)
}

You can use a callback instead of a binding:

DynamicUI(json: json) { component in
    print(component.eventHandler as Any, component.state as Any)
}

Custom components

Render application-specific component types without forking DynamicUI. Return nil for component types your renderer does not recognize:

DynamicUI(
    json: json,
    component: $component,
    error: $error,
    customViewRenderer: { component in
        guard component.type == "ProductCard" else { return nil }

        return AnyView(
            ProductCard(
                title: component.title ?? "Product",
                productID: component.parameters?["productID"]?.toString()
            )
        )
    }
)

DynamicUI applies supported modifiers and accessibility metadata to the custom view. Unknown types that neither DynamicUI nor the application recognizes are logged and skipped.

Accessibility

DynamicUI uses native SwiftUI controls and supports explicit assistive-technology metadata:

{
    "type": "Button",
    "title": "Save",
    "accessibilityLabel": "Save profile",
    "accessibilityHint": "Saves your profile changes",
    "accessibilityValue": "Ready",
    "accessibilityIdentifier": "profile.save",
    "accessibilityInputLabels": ["Save", "Save profile"]
}

Use accessibilityHidden: true only for decorative content. Visible control titles continue to provide native semantics when explicit accessibility metadata is not needed. Accessibility string fields support the same conditional expressions as visible strings.

JSON schema

Every component requires a case-sensitive type. Common optional fields are:

| Field | Purpose | | --- | --- | | title | Label, title, placeholder, or image description | | if | Identifier condition such as $showDetails that controls rendering | | identifier | Stable key for updates and conditional expressions | | eventHandler | Application-defined event name returned on interaction | | defaultValue | Initial value for stateful controls | | children | Nested component array for containers | | url | SF Symbol name or URL, depending on the component | | disabled | Disables the component | | modifiers | Visual and behavioral modifiers | | minimumValue, maximumValue | Numeric bounds for sliders and progress views | | accessibilityLabel | Concise, speakable name for assistive technologies | | accessibilityHint | Describes the result of interacting with the component | | accessibilityValue | Accessible state or formatted value | | accessibilityIdentifier | Stable identifier for UI automation | | accessibilityHidden | Hides decorative content from assistive technologies | | accessibilityInputLabels | Alternative spoken names for Voice Control |

Unknown component types are logged and skipped, allowing valid sibling components to keep rendering. Decode and validation failures are written to the optional error binding and display a fallback error view. Objects without a string type field are treated as metadata and ignored in component arrays.

Playground and documentation

The Playground directory contains an Xcode project with basic and exhaustive JSON examples for macOS, iOS, watchOS, tvOS, and visionOS.

See the complete schema, platform behavior, component examples, and modifier reference in the documentation.

Used By

Using DynamicUI in your project? Open a pull request to add it here.

Contributing

Issues and pull requests are welcome. See CONTRIBUTING.md for setup, testing, and contribution guidance, or browse the good first issue label.

Contact

πŸ¦‹ @0xWDG 🐘 mastodon.social/@0xWDG 🐦 @0xWDG 🧡 @0xWDG 🌐 wesleydegroot.nl πŸ€– Discord

Package Metadata

Repository: 0xwdg/dynamicui

Default branch: main

README: README.md