JSONDrivenUI
Build native SwiftUI views from JSON — or convert SwiftUI code back to JSON.
Features
- JSON to SwiftUI — Render dynamic UI from JSON data at runtime
- SwiftUI DSL to JSON —
@resultBuilderDSL that looks like SwiftUI but produces JSON - 21 view types — Text, Image, Button, Toggle, TextField, NavigationStack, Grid, and more
- 25+ modifiers — Padding, colors, shadows, blur, rotation, corner radius, accessibility...
- Action callbacks — Handle button taps, toggle changes, and text field submissions
- Accessibility — Labels, hints, and hidden support baked in
- 160 unit tests — Comprehensive coverage across all components
Screenshots
| Basic Views | Styling & Modifiers | Interactive Elements | |:-----------:|:-------------------:|:-------------------:| | !Basic | !Styling | !Interactive |
| SwiftUI-to-JSON Editor | Live JSON Editor | Builder DSL Round-Trip | |:----------------------:|:----------------:|:---------------------:| | !SwiftUI to JSON | !Live Editor | !Builder DSL |
Installation
Swift Package Manager
dependencies: [
.package(url: "https://github.com/EnesKaraosman/JSONDrivenUI.git", from: "1.0.0")
]
Quick Start
JSON to SwiftUI
import JSONDrivenUI
// From a JSON string
JSONDataView(jsonString: """
{
"type": "VStack",
"properties": { "spacing": 12, "padding": 16 },
"subviews": [
{ "type": "Text", "values": { "text": "Hello World" }, "properties": { "font": "title" } },
{ "type": "Image", "values": { "systemIconName": "star.fill" }, "properties": { "height": 40, "foregroundColor": "#FFD700" } }
]
}
""")
// With action handling
JSONDataView(jsonString: myJSON) { actionId in
print("Action triggered: \(actionId)")
}
SwiftUI DSL to JSON
import JSONDrivenUI
let json = buildJSONString {
VStackNode(spacing: 16) {
TextNode("Hello World", font: "title", fontWeight: "bold")
.foregroundColor("#007AFF")
HStackNode(spacing: 8) {
ImageNode(systemName: "star.fill")
.foregroundColor("#FFD700")
.frame(width: 24, height: 24)
TextNode("Favorited")
}
ButtonNode("Tap Me", actionId: "my_action")
.padding(12)
.backgroundColor("#007AFF")
.foregroundColor("#FFFFFF")
.cornerRadius(10)
}
.padding(20)
}
// Render the generated JSON right back
JSONDataView(jsonString: json)
Supported View Types
| Type | Description | Key Values / Properties |
|------|-------------|------------------------|
| Text | Text label | text, font, fontWeight |
| Image | System, local, or remote | systemIconName, localImageName, imageUrl |
| Button | Tappable | actionId, text, or use subviews as label |
| Toggle | On/off switch | text, isOn, actionId |
| TextField | Text input | placeholder, actionId |
| VStack | Vertical stack | spacing, horizontalAlignment |
| HStack | Horizontal stack | spacing, verticalAlignment |
| ZStack | Depth stack | — |
| LazyVStack | Lazy vertical stack | spacing, horizontalAlignment |
| LazyHStack | Lazy horizontal stack | spacing, verticalAlignment |
| ScrollView | Scrollable container | axis, showsIndicators |
| List | List container | — |
| Grid | Grid layout | spacing |
| GridRow | Row inside Grid | — |
| NavigationStack | Navigation container | — |
| NavigationLink | Push navigation | text, first subview = destination |
| Color | Solid color fill | foregroundColor |
| Rectangle | Rectangle shape | — |
| Circle | Circle shape | — |
| Spacer | Flexible space | minLength |
| Divider | Line separator | — |
Supported Properties
Layout
| Property | Type | Description | |----------|------|-------------| |padding | Int | All-edge padding |
| spacing | Int | Stack spacing |
| width / height | Float | Fixed dimensions |
| maxWidth / maxHeight | Float | Maximum dimensions |
| minLength | Float | Spacer minimum length |
Appearance
| Property | Type | Description | |----------|------|-------------| |foregroundColor | String | Hex color (e.g. #FF0000) |
| backgroundColor | String | Hex color |
| font | String | largeTitle, title, headline, subheadline, body, callout, footnote, caption |
| fontWeight | String | ultraLight, thin, light, regular, medium, semibold, bold, heavy, black |
| opacity | Float | 0.0 to 1.0 |
| grayscale | Float | 0.0 to 1.0 |
| blur | Float | Blur radius |
| rotation | Float | Degrees |
Shape & Border
| Property | Type | Description | |----------|------|-------------| |cornerRadius | Float | Corner rounding |
| clipShape | String | circle, capsule, rectangle |
| borderColor | String | Hex color |
| borderWidth | Int | Border thickness |
Shadow
| Property | Type | Description | |----------|------|-------------| |shadowRadius | Float | Shadow blur |
| shadowColor | String | Hex color |
| shadowX / shadowY | Float | Shadow offset |
Alignment (Stacks)
| Property | Type | Description | |----------|------|-------------| |horizontalAlignment | String | leading, center, trailing |
| verticalAlignment | String | top, center, bottom, firstTextBaseline, lastTextBaseline |
| axis | String | vertical, horizontal (ScrollView) |
| showsIndicators | Bool | ScrollView indicators |
Accessibility
| Property | Type | Description | |----------|------|-------------| |accessibilityLabel | String | VoiceOver label |
| accessibilityHint | String | VoiceOver hint |
| accessibilityHidden | Bool | Hide from VoiceOver |
Action Handling
Interactive elements fire a callback with their actionId:
JSONDataView(jsonString: """
{
"type": "VStack",
"subviews": [
{ "type": "Button", "values": { "text": "Save", "actionId": "save" } },
{ "type": "Toggle", "values": { "text": "Notifications", "isOn": true, "actionId": "notif_toggle" } },
{ "type": "TextField", "values": { "placeholder": "Search...", "actionId": "search" } }
]
}
""") { actionId in
switch actionId {
case "save": print("Save tapped")
case "notif_toggle": print("Toggle changed")
case "search": print("Search submitted")
default: break
}
}
Builder DSL Reference
All node types mirror their JSON counterparts:
VStackNode(alignment:spacing:) { } HStackNode(alignment:spacing:) { }
ZStackNode { } ScrollViewNode(axis:showsIndicators:) { }
ListNode { } GridNode(spacing:) { }
GridRowNode { } NavigationStackNode { }
TextNode("text", font:fontWeight:) ImageNode(systemName:) / ImageNode(url:) / ImageNode(localName:)
ButtonNode("text", actionId:) ButtonNode(actionId:) { / label / }
ToggleNode("label", isOn:actionId:) TextFieldNode(placeholder:text:actionId:)
NavigationLinkNode("text") { / destination / }
SpacerNode(minLength:) DividerNode()
RectangleNode() CircleNode()
ColorNode(hex:)
Chainable modifiers:
.padding(16) .frame(width:height:) .maxFrame(width:height:)
.foregroundColor("#hex") .backgroundColor("#hex") .cornerRadius(8)
.clipShape("circle") .opacity(0.8) .rotation(45)
.blur(3) .grayscale(0.5) .shadow(radius:color:x:y:)
.border(color:width:) .font("title") .fontWeight("bold")
.accessibilityLabel("") .accessibilityHint("") .accessibilityHidden()
Error Handling
Invalid JSON shows descriptive errors in debug builds:
// Debug: shows "JSON decoding failed: Missing key 'type' at ..."
// Release: shows "Failed to load view"
JSONDataView(jsonString: "{ invalid json }")
A recursion depth limit of 50 prevents stack overflow from deeply nested structures.
Example App
The Example/ directory contains a multiplatform demo app (iOS + macOS) with:
- Basic — Text, Image, SF Symbols
- Layout — HStack, VStack, ZStack, Grid, LazyVStack, ScrollView
- Interactive — Button with callbacks, Toggle, TextField
- Styling — Shadows, rounded corners, opacity, blur, rotation, grayscale
- Navigation — NavigationStack with NavigationLinks
- SwiftUI to JSON — Write SwiftUI-like code with syntax highlighting, see the generated JSON and live preview
- Live Editor — Edit JSON directly with syntax highlighting and see it rendered in real-time
- Builder DSL — Round-trip demo: Swift DSL builds JSON, then renders it
Requirements
- iOS 17.0+ / macOS 14.0+
- Swift 5.9+
- Xcode 15.0+
Dependencies
- Kingfisher 8.0+ — Remote image loading and caching
License
MIT License. See LICENSE for details.