Skip to content
Merged
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
254 changes: 252 additions & 2 deletions Playground/DynamicUI Playground/ContentView.swift

Large diffs are not rendered by default.

51 changes: 28 additions & 23 deletions Playground/DynamicUI Watch App/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,39 @@ import DynamicUI

struct ContentView: View {
@State
var text: String = """
private var text: String = """
[
{
"type": "VStack",
"children": [
{
"type": "Button",
"title": "Click me",
"eventHandler": "customHandler"
},
{
"type": "Toggle",
"title": "Toggle me",
"identifier": "my.toggle.1"
},
{
"type": "Text",
"title": "_Wait_, am i generating views from JSON?",
"modifiers": {
"foregroundStyle": "red",
"opacity": 0.5
{
"type": "Button",
"title": "Click me",
"eventHandler": "customHandler"
},
},
{
"type": "Label",
"title": "Shine",
"url": "star"
}
{
"type": "Toggle",
"title": "Toggle me",
"identifier": "myIdentifier"
},
{
"type": "Text",
"title": "_Wait_, am i generating views from JSON?",
"modifiers": {
"foregroundStyle": "red",
"opacity": 0.5
}
},
{
"type": "Label",
"title": "Shine",
"url": "{$myIdentifier ? star.fill : star}"
},
{
"type": "Text",
"title": "The toggle is enabled",
"if": "$myIdentifier"
}
]
}
]
Expand Down
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# DynamicUI

Create SwiftUI interfaces from JSON component trees. DynamicUI supports nested layouts,
interactive controls, runtime value updates, conditional strings, and a focused set of SwiftUI
modifiers.
interactive controls, runtime value updates, conditional strings and views, and a focused set of
SwiftUI modifiers.

[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2F0xWDG%2FDynamicUI%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/0xWDG/DynamicUI) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2F0xWDG%2FDynamicUI%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/0xWDG/DynamicUI)
[![Swift Package Manager](https://img.shields.io/badge/SPM-compatible-brightgreen.svg)](https://swift.org/package-manager)
Expand Down Expand Up @@ -87,13 +87,21 @@ Conditional expressions can select a string value using another component's curr
"type": "Label",
"title": "Shine",
"url": "{$myIdentifier ? star.fill : star}"
},
{
"type": "Text",
"title": "The toggle is enabled",
"if": "$myIdentifier"
}
]
```

The syntax is `{$identifier ? valueWhenTrue : valueWhenFalse}`. It works in string-valued
component fields, including `title`, `text`, `url`, modifiers, and parameters.

Set `"if": "$identifier"` to render an entire component only while the referenced value is
truthy. Missing identifiers and empty, zero, `false`, or `null` values hide the component.

## Handle interactions

The `component` binding receives the latest interacted-with component. Stateful controls include
Expand Down Expand Up @@ -158,6 +166,7 @@ Every component requires a case-sensitive `type`. Common optional fields are:
| Field | Purpose |
| --- | --- |
| `title` | Label, title, placeholder, or accessibility label |
| `if` | Identifier condition such as `$myIdentifier` that controls whether the component renders |
| `identifier` | Stable key for updates and conditional expressions |
| `eventHandler` | Application-defined event name returned on interaction |
| `defaultValue` | Initial value for stateful controls |
Expand All @@ -167,8 +176,9 @@ Every component requires a case-sensitive `type`. Common optional fields are:
| `modifiers` | Visual and behavioral modifiers |
| `minimumValue`, `maximumValue` | Numeric bounds for sliders and progress views |

Unknown component types render no view. Decode failures are written to the optional `error`
binding and display a fallback error view.
Unknown component types are logged and skipped, allowing valid sibling components to keep rendering.
Decode 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 silently ignored in component arrays.

## Playground application

Expand Down
104 changes: 72 additions & 32 deletions Sources/DynamicUI/DynamicUI.docc/DynamicUI.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@ contains the new value for controls such as toggles, text fields, pickers, and s

## Component Schema

Every object requires a case-sensitive `type`. All other fields are optional and are used only
by components that support them.
Every component object requires a case-sensitive string `type`. Objects without a string `type`
are treated as metadata and ignored, including inside `children`. All other fields are optional
and are used only by components that support them.

| Field | JSON type | Purpose |
| --- | --- | --- |
| `type` | String | SwiftUI view type to render. Unknown types render no view. |
| `type` | String | SwiftUI view type to render. Unknown types are logged and render no view. |
| `title` | String | Label, title, placeholder, or accessibility label, depending on the view. |
| `text` | String | Additional string value available to components. |
| `if` | String | Renders the component only when the referenced identifier is truthy. |
| `identifier` | String | Stable key used in interaction updates and conditional expressions. |
| `eventHandler` | String | Application-defined event name returned with the component. |
| `defaultValue` | String, number, Boolean, or object | Initial value for stateful controls. |
Expand Down Expand Up @@ -108,45 +110,76 @@ entire string. They work in `title`, `text`, `url`, `minimum`, `maximum`, and st
in `modifiers` or `parameters`. Missing identifiers and empty, zero, `false`, or `null` values
select the false branch.

## Conditional Views

Use `if` with an identifier prefixed by `$` to include a component only while that identifier's
current value is truthy:

```json
{
"type": "Text",
"title": "The option is enabled",
"if": "$enabled"
}
```

Missing identifiers and empty, zero, `false`, or `null` values hide the component. Omitting `if`
renders the component normally.

## Supported Views

### Content

- <doc:Views/Text>
- <doc:Views/Label>
- <doc:Views/Image>
- <doc:Views/Divider>
- <doc:Views/Spacer>
- <doc:Text>
- <doc:Label>
- <doc:Image>
- <doc:AsyncImage>
- <doc:Divider>
- <doc:Spacer>
- <doc:Link>

### Controls

- <doc:Views/Button>
- <doc:Views/Toggle>
- <doc:Views/TextField>
- <doc:Views/SecureField>
- <doc:Views/TextEditor>
- <doc:Views/Picker>
- <doc:Views/Slider>
- <doc:Views/Gauge>
- <doc:Views/ProgressView>
- <doc:Button>
- <doc:Toggle>
- <doc:TextField>
- <doc:SecureField>
- <doc:TextEditor>
- <doc:Picker>
- <doc:Slider>
- <doc:Gauge>
- <doc:ProgressView>
- <doc:DatePicker>
- <doc:Stepper>
- <doc:ColorPicker>
- <doc:Menu>
- <doc:ShareLink>

### Containers

- <doc:Views/VStack>
- <doc:Views/HStack>
- <doc:Views/ZStack>
- <doc:Views/Group>
- <doc:Views/GroupBox>
- <doc:Views/DisclosureGroup>
- <doc:Views/List>
- <doc:Views/Form>
- <doc:Views/Section>
- <doc:Views/ScrollView>
- <doc:Views/NavigationView>
- <doc:Views/NavigationSplitView>
- <doc:Views/TabView>
- <doc:Views/HSplitView>
- <doc:Views/VSplitView>
- <doc:VStack>
- <doc:HStack>
- <doc:ZStack>
- <doc:LazyVStack>
- <doc:LazyHStack>
- <doc:LazyVGrid>
- <doc:LazyHGrid>
- <doc:Grid>
- <doc:GridRow>
- <doc:Group>
- <doc:GroupBox>
- <doc:DisclosureGroup>
- <doc:List>
- <doc:Form>
- <doc:Section>
- <doc:ScrollView>
- <doc:NavigationView>
- <doc:NavigationStack>
- <doc:NavigationLink>
- <doc:NavigationSplitView>
- <doc:TabView>
- <doc:HSplitView>
- <doc:VSplitView>

## Platform Behavior

Expand All @@ -157,6 +190,13 @@ Some views adapt when their SwiftUI equivalent is unavailable:
| `NavigationSplitView` | Uses `NavigationView` before iOS 16, macOS 13, and tvOS 16, and on watchOS. |
| `HSplitView` | Uses `HStack` outside macOS. |
| `VSplitView` | Renders only on macOS. |
| `NavigationStack` | Uses `NavigationView` before iOS 16, macOS 13, tvOS 16, and watchOS 9. |
| `Grid`, `GridRow` | Use stack-based layouts before iOS 16, macOS 13, tvOS 16, and watchOS 9. |
| `Menu` | Uses an accessible vertical group where native menus are unavailable. |
| `ShareLink` | Uses `Link` on tvOS and older systems. |
| `ColorPicker` | Renders only on iOS and macOS. |
| `DatePicker` | Renders no control on tvOS and requires watchOS 10. |
| `Stepper` | Renders no control on tvOS and requires watchOS 9. |
| `DisclosureGroup`, `Group`, `GroupBox` | Use `VStack` on tvOS and watchOS. |
| `TextEditor` | Uses `TextField` outside iOS and macOS. |
| `Slider` | Renders no view on tvOS. |
Expand Down
23 changes: 23 additions & 0 deletions Sources/DynamicUI/DynamicUI.docc/Views/AsyncImage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# AsyncImage Example

Load a remote image with a progress placeholder and an accessible failure state.

```json
[
{
"type": "AsyncImage",
"title": "DynamicUI logo",
"url": "https://example.com/image.png",
"parameters": {
"contentMode": "fit"
}
}
]
```

| Parameter | Type | Description |
| --- | --- | --- |
| `title` | String | Accessibility label and loading description. |
| `url` | String | Remote HTTP or HTTPS image URL. |
| `parameters.contentMode` | String | `fit` (default) or `fill`. |
| `modifiers` | Object | Visual and behavioral modifiers. |
26 changes: 26 additions & 0 deletions Sources/DynamicUI/DynamicUI.docc/Views/ColorPicker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ColorPicker Example

```json
[
{
"type": "ColorPicker",
"title": "Accent color",
"identifier": "accent",
"defaultValue": "#3366FFFF",
"parameters": {
"supportsOpacity": true
}
}
]
```

Changes emit an eight-digit `#RRGGBBAA` hexadecimal string in `state`. ColorPicker renders on iOS
and macOS and is unavailable on tvOS and watchOS.

| Parameter | Type | Description |
| --- | --- | --- |
| `title` | String | Accessible picker label. |
| `identifier` | String | Key used for updates and conditional expressions. |
| `defaultValue` | String | Named color, `#RRGGBB`, or `#RRGGBBAA`. |
| `parameters.supportsOpacity` | Boolean | Whether the picker includes opacity. Defaults to `true`. |
| `modifiers` | Object | Visual and behavioral modifiers. |
29 changes: 29 additions & 0 deletions Sources/DynamicUI/DynamicUI.docc/Views/DatePicker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# DatePicker Example

```json
[
{
"type": "DatePicker",
"title": "Starts",
"identifier": "startDate",
"defaultValue": "2026-08-20T09:00:00Z",
"minimum": "2026-01-01T00:00:00Z",
"maximum": "2026-12-31T23:59:59Z",
"parameters": {
"displayedComponents": "dateAndTime"
}
}
]
```

Dates use ISO 8601 strings. A numeric `defaultValue` is interpreted as seconds since 1970. Changes
emit an ISO 8601 string in `state`. DatePicker is unavailable on tvOS and requires watchOS 10.

| Parameter | Type | Description |
| --- | --- | --- |
| `title` | String | Accessible picker label. |
| `identifier` | String | Key used for updates and conditional expressions. |
| `defaultValue` | String or Number | Initial ISO 8601 date or Unix timestamp. |
| `minimum`, `maximum` | String | Optional inclusive ISO 8601 bounds. |
| `parameters.displayedComponents` | String | `date`, `hourAndMinute`, or `dateAndTime` (default). |
| `modifiers` | Object | Visual and behavioral modifiers. |
31 changes: 31 additions & 0 deletions Sources/DynamicUI/DynamicUI.docc/Views/Grid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Grid Example

```json
[
{
"type": "Grid",
"parameters": {
"horizontalSpacing": 12,
"verticalSpacing": 8
},
"children": [
{
"type": "GridRow",
"children": [
{ "type": "Text", "title": "Name" },
{ "type": "Text", "title": "Value" }
]
}
]
}
]
```

Grid requires iOS 16, macOS 13, tvOS 16, or watchOS 9 and falls back to VStack on older systems.

| Parameter | Type | Description |
| --- | --- | --- |
| `parameters.horizontalSpacing` | Number | Optional spacing between columns. |
| `parameters.verticalSpacing` | Number | Optional spacing between rows. |
| `children` | Array | GridRow components. |
| `modifiers` | Object | Visual and behavioral modifiers. |
20 changes: 20 additions & 0 deletions Sources/DynamicUI/DynamicUI.docc/Views/GridRow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# GridRow Example

```json
[
{
"type": "GridRow",
"children": [
{ "type": "Text", "title": "Name" },
{ "type": "Text", "title": "Value" }
]
}
]
```

GridRow requires iOS 16, macOS 13, tvOS 16, or watchOS 9 and falls back to HStack on older systems.

| Parameter | Type | Description |
| --- | --- | --- |
| `children` | Array | Cells in the row. |
| `modifiers` | Object | Visual and behavioral modifiers. |
Loading
Loading