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
15 changes: 7 additions & 8 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,17 @@ on:
branches: [ main ]

jobs:
build:

runs-on: macos-14
build-and-test:
runs-on: macos-15

steps:
- uses: actions/checkout@v4

- name: List Xcode installations
run: sudo ls -1 /Applications | grep "Xcode"
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_16.0.app/Contents/Developer

- name: Select Xcode 15.2
run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer

- name: Build
run: swift build -v

- name: Test
run: swift test -v
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/Packages
/*.xcodeproj
xcuserdata/
.swiftpm/
7 changes: 0 additions & 7 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

This file was deleted.

This file was deleted.

96 changes: 96 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Changelog

All notable changes to JSONDrivenUI will be documented in this file.

## [1.0.0] - 2026-04-11

### Breaking Changes
- Minimum deployment targets raised to **iOS 17.0** / **macOS 14.0**
- Swift tools version bumped to **5.9**
- Kingfisher dependency bumped to **8.0+**
- `ViewProperties` defaults changed from hardcoded values to `nil` (e.g., `foregroundColor` no longer defaults to white)
- `Color.init(hex:)` is now failable (`init?`) — returns `nil` for invalid hex instead of white
- `ViewMaterial`, `ViewProperties`, `Values`, and `ViewType` are now `public`
- Removed `View+Extensions.swift` (`embedInAnyView()` helper)

### Added

#### New View Types
- **Button** — with `actionId` for tap callbacks and optional subview labels
- **Toggle** — on/off switch with `isOn` default state and `actionId`
- **TextField** — text input with `placeholder` and submit callback
- **NavigationStack** — modern navigation container (replaces deprecated NavigationView)
- **NavigationLink** — push navigation with destination/label subviews
- **Color** — solid color fill view
- **Grid** — grid layout container
- **GridRow** — row within a Grid

#### New Modifiers
- `backgroundColor` — background color (hex)
- `cornerRadius` — uses modern `.clipShape(.rect(cornerRadius:))` API
- `clipShape` — clip to `circle`, `capsule`, or `rectangle`
- `opacity` — transparency (0.0–1.0)
- `shadowRadius`, `shadowColor`, `shadowX`, `shadowY` — drop shadow
- `rotation` — rotation in degrees
- `blur` — Gaussian blur
- `grayscale` — desaturation (0.0–1.0)
- `maxWidth`, `maxHeight` — maximum frame dimensions

#### Accessibility
- `accessibilityLabel` — VoiceOver label
- `accessibilityHint` — VoiceOver hint
- `accessibilityHidden` — hide from VoiceOver

#### Action Callback System
- New `ActionCallback` type alias and `onAction` parameter on `JSONDataView`
- Buttons, Toggles, and TextFields fire callbacks with their `actionId`
- Stateful wrappers (`ToggleWrapper`, `TextFieldWrapper`) manage `@State` internally

#### Builder DSL (SwiftUI-like to JSON)
- `@resultBuilder`-based DSL for building JSON programmatically
- Node types: `VStackNode`, `HStackNode`, `ZStackNode`, `TextNode`, `ImageNode`, `ButtonNode`, `ToggleNode`, `TextFieldNode`, `ScrollViewNode`, `ListNode`, `GridNode`, `GridRowNode`, `NavigationStackNode`, `NavigationLinkNode`, `SpacerNode`, `DividerNode`, `RectangleNode`, `CircleNode`, `ColorNode`
- Chainable modifiers: `.padding()`, `.foregroundColor()`, `.backgroundColor()`, `.cornerRadius()`, `.opacity()`, `.shadow()`, `.rotation()`, `.blur()`, `.grayscale()`, `.frame()`, `.maxFrame()`, `.border()`, `.clipShape()`, `.font()`, `.fontWeight()`, `.accessibilityLabel()`, `.accessibilityHint()`, `.accessibilityHidden()`
- `buildJSON()` and `buildJSONString()` top-level functions

#### Error Handling
- `JSONDrivenUIError` enum with `.decodingFailed`, `.maxDepthExceeded`, `.fileNotFound`
- Descriptive error messages in debug builds showing exact decoding failure path
- Generic fallback message in release builds
- Recursion depth limit (max 50 levels) prevents stack overflow

#### Convenience
- `JSONDataView(jsonString:onAction:)` initializer
- `ViewFactory` depth limiting with `maxDepth` constant

#### Example App
- 6-tab multiplatform demo app (iOS + macOS)
- **Basic** — Text, Image, SF Symbols layout
- **Layout** — HStack, VStack, ZStack, Grid, LazyVStack, ScrollView
- **Interactive** — Button, Toggle, TextField with action callbacks
- **Styling** — Shadows, corners, opacity, blur, rotation, grayscale
- **Navigation** — NavigationStack with NavigationLinks
- **More** — SwiftUI-to-JSON editor (syntax highlighted), Live JSON editor (syntax highlighted), Builder DSL round-trip demo

#### Tests
- 160 unit tests across 11 test files
- Coverage: color parsing, JSON decoding, property defaults, builder DSL, node types, node modifiers, JSON export, view factory, error handling, depth limiting

### Fixed
- Copy-paste error in `ViewFactory`: HStack showed "LazyHStack" error message and vice versa
- `ViewProperties` defaults were hardcoded (white foreground, red border) — now `nil`
- `fontWeight` default was `"body"` (invalid weight) — now `nil`
- Example app image URLs changed from `http://` to `https://`

### Improved
- `ViewFactory` annotated with `@MainActor` for Kingfisher 8.x compatibility
- `PresentableProtocol` annotated with `@MainActor`
- Model types marked `final` and `@unchecked Sendable` for Swift 6 readiness
- All model properties made `public` for external consumers
- CI updated to run tests (`swift test -v`) on macOS 15 with Xcode 16
- README rewritten with badges, screenshot gallery, organized property tables, and DSL reference

---

## [1.x] - Previous Releases

See [git history](https://github.com/EnesKaraosman/JSONDrivenUI/commits/main) for changes prior to 1.0.0.
32 changes: 32 additions & 0 deletions Example/Example.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,25 @@
F4F42B862B322F60002433B4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4F42B852B322F60002433B4 /* Assets.xcassets */; };
F4F42B8A2B322F60002433B4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F4F42B892B322F60002433B4 /* Preview Assets.xcassets */; };
F4F42B942B323073002433B4 /* JSONDrivenUI in Frameworks */ = {isa = PBXBuildFile; productRef = F4F42B932B323073002433B4 /* JSONDrivenUI */; };
AA000001000000000000000A /* BasicSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000001000000000000000B /* BasicSample.swift */; };
AA000002000000000000000A /* LayoutSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000002000000000000000B /* LayoutSample.swift */; };
AA000003000000000000000A /* InteractiveSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000003000000000000000B /* InteractiveSample.swift */; };
AA000004000000000000000A /* StylingSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000004000000000000000B /* StylingSample.swift */; };
AA000005000000000000000A /* NavigationSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000005000000000000000B /* NavigationSample.swift */; };
AA000006000000000000000A /* BuilderSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000006000000000000000B /* BuilderSample.swift */; };
AA000007000000000000000A /* LiveEditorSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000007000000000000000B /* LiveEditorSample.swift */; };
AA000008000000000000000A /* SwiftUIParserSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000008000000000000000B /* SwiftUIParserSample.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
AA000001000000000000000B /* BasicSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicSample.swift; sourceTree = "<group>"; };
AA000002000000000000000B /* LayoutSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutSample.swift; sourceTree = "<group>"; };
AA000003000000000000000B /* InteractiveSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveSample.swift; sourceTree = "<group>"; };
AA000004000000000000000B /* StylingSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StylingSample.swift; sourceTree = "<group>"; };
AA000005000000000000000B /* NavigationSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSample.swift; sourceTree = "<group>"; };
AA000006000000000000000B /* BuilderSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderSample.swift; sourceTree = "<group>"; };
AA000007000000000000000B /* LiveEditorSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveEditorSample.swift; sourceTree = "<group>"; };
AA000008000000000000000B /* SwiftUIParserSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIParserSample.swift; sourceTree = "<group>"; };
F470BA722B3232C700519DAA /* Complex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Complex.swift; sourceTree = "<group>"; };
F470BA742B32343300519DAA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
F4F42B7E2B322F5E002433B4 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand All @@ -42,6 +58,14 @@
isa = PBXGroup;
children = (
F470BA722B3232C700519DAA /* Complex.swift */,
AA000001000000000000000B /* BasicSample.swift */,
AA000002000000000000000B /* LayoutSample.swift */,
AA000003000000000000000B /* InteractiveSample.swift */,
AA000004000000000000000B /* StylingSample.swift */,
AA000005000000000000000B /* NavigationSample.swift */,
AA000006000000000000000B /* BuilderSample.swift */,
AA000007000000000000000B /* LiveEditorSample.swift */,
AA000008000000000000000B /* SwiftUIParserSample.swift */,
);
path = Samples;
sourceTree = "<group>";
Expand Down Expand Up @@ -163,6 +187,14 @@
F4F42B842B322F5E002433B4 /* ContentView.swift in Sources */,
F470BA732B3232C700519DAA /* Complex.swift in Sources */,
F4F42B822B322F5E002433B4 /* ExampleApp.swift in Sources */,
AA000001000000000000000A /* BasicSample.swift in Sources */,
AA000002000000000000000A /* LayoutSample.swift in Sources */,
AA000003000000000000000A /* InteractiveSample.swift in Sources */,
AA000004000000000000000A /* StylingSample.swift in Sources */,
AA000005000000000000000A /* NavigationSample.swift in Sources */,
AA000006000000000000000A /* BuilderSample.swift in Sources */,
AA000007000000000000000A /* LiveEditorSample.swift in Sources */,
AA000008000000000000000A /* SwiftUIParserSample.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 57 additions & 1 deletion Example/Example/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,64 @@ import SwiftUI
import JSONDrivenUI

struct ContentView: View {
@State private var lastAction: String = ""

var body: some View {
TabView {
ScrollView {
JSONDataView(jsonString: basicSample)
}
.tabItem { Label("Basic", systemImage: "text.alignleft") }

JSONDataView(jsonString: layoutSample)
.tabItem { Label("Layout", systemImage: "square.grid.2x2") }

VStack {
JSONDataView(jsonString: interactiveSample) { actionId in
lastAction = actionId
}
if !lastAction.isEmpty {
Text("Action: \(lastAction)")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.bottom, 4)
}
}
.tabItem { Label("Interactive", systemImage: "hand.tap") }

JSONDataView(jsonString: stylingSample)
.tabItem { Label("Styling", systemImage: "paintbrush") }

JSONDataView(jsonString: navigationSample)
.tabItem { Label("Navigation", systemImage: "arrow.right.circle") }

MoreSamplesView()
.tabItem { Label("More", systemImage: "ellipsis.circle") }
}
}
}

struct MoreSamplesView: View {
var body: some View {
JSONDataView(json: Data(complexSample.utf8))
NavigationStack {
List {
NavigationLink("SwiftUI to JSON") {
SwiftUIParserSampleView()
}
NavigationLink("Builder DSL") {
BuilderSampleView()
}
NavigationLink("Live Editor") {
LiveEditorSampleView()
}
NavigationLink("Complex (Original)") {
ScrollView {
JSONDataView(jsonString: complexSample)
}
}
}
.navigationTitle("More Samples")
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions Example/Example/ExampleApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ struct ExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
#if os(macOS)
.frame(minWidth: 800, minHeight: 600)
#endif
}
}
}
82 changes: 82 additions & 0 deletions Example/Example/Samples/BasicSample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// BasicSample.swift
// Example
//

import Foundation

let basicSample = """
{
"type": "VStack",
"properties": {
"spacing": 16,
"horizontalAlignment": "center",
"padding": 20
},
"subviews": [
{
"type": "Text",
"properties": {
"font": "largeTitle",
"fontWeight": "bold"
},
"values": {
"text": "Welcome!"
}
},
{
"type": "Text",
"properties": {
"font": "body",
"foregroundColor": "#666666"
},
"values": {
"text": "This is a basic example showing Text, Image, and layout."
}
},
{
"type": "Image",
"values": {
"systemIconName": "star.fill"
},
"properties": {
"height": 60,
"width": 60,
"foregroundColor": "#FFD700"
}
},
{
"type": "Divider"
},
{
"type": "HStack",
"properties": {
"spacing": 12
},
"subviews": [
{
"type": "Image",
"values": { "systemIconName": "person.circle.fill" },
"properties": { "height": 40, "width": 40, "foregroundColor": "#007AFF" }
},
{
"type": "VStack",
"properties": { "horizontalAlignment": "leading", "spacing": 2 },
"subviews": [
{
"type": "Text",
"properties": { "fontWeight": "semibold" },
"values": { "text": "JSON Driven UI" }
},
{
"type": "Text",
"properties": { "font": "caption", "foregroundColor": "#999999" },
"values": { "text": "Build UIs from JSON" }
}
]
}
]
}
]
}
"""
Loading
Loading