eneskaraosman/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.
Package Metadata
Repository: eneskaraosman/jsondrivenui
Default branch: main
README: README.md