Converter Command - Tai-Kimura/SwiftJsonUI GitHub Wiki
The sjui g converter command generates custom SwiftUI components that work in both Static and Dynamic modes. This powerful generator creates all the necessary files to build and use custom components in your SwiftJsonUI project.
- Overview
- Basic Usage
- Command Options
- Generated Files
- Examples
- Configuration
- Best Practices
- Troubleshooting
The converter generator creates three essential files that work together:
- Swift Component - The actual SwiftUI View
- Ruby Converter - Converts JSON to Swift code for Static mode
- Dynamic Adapter - Enables Dynamic mode with hot reload
This is the primary way to extend SwiftJsonUI with custom, reusable components.
sjui g converter MyButtonsjui g converter MyCard --attributes "title:String,subtitle:String,imageUrl:String"sjui g converter MyContainer --containersjui g converter MyBadge --no-containerDefines the properties for your component. You can specify multiple attributes either comma-separated in a single string or by using the option multiple times.
Format: "name:Type,name:Type,..." or multiple --attributes name:Type
Supported Types:
-
String- Text values (Swift String type) -
Int/Integer- Whole numbers (Swift Int type) -
Double/Float- Decimal numbers (Swift Double type) -
Bool/Boolean- True/false values (Swift Bool type) -
Color- Color values (SwiftUI Color type, accepts hex strings like "#FF0000" in JSON) -
EdgeInsets- Padding/margin values (SwiftUI EdgeInsets type)
Examples:
# Single string with comma-separated attributes
sjui g converter ProfileCard --attributes "name:String,age:Int,verified:Bool,avatarColor:Color"
# Multiple --attributes options
sjui g converter ProfileCard \
--attributes name:String \
--attributes age:Int \
--attributes verified:Bool \
--attributes avatarColor:ColorHow attributes work:
- Each attribute becomes a parameter in the Swift component's initializer
- The Ruby converter automatically handles JSON-to-Swift conversion for each type
- The Dynamic adapter accesses attributes via
component.rawData["attributeName"] - Boolean attributes use
@component.key?('attributeName')to detect presence (supporting nil/false distinction)
Prefix attribute names with @ to create SwiftUI binding properties that can be modified by the parent view.
Format: "@propertyName:Type"
Example:
# Generate a component with a binding property
sjui g converter UserProfile --attributes "@user:User,@isEditing:Bool,showDetails:Bool"Generated Swift Component:
struct UserProfile<Content: View>: View {
@SwiftUI.Binding var user: User
@SwiftUI.Binding var isEditing: Bool
let showDetails: Bool // Regular property (not binding)
let content: Content?
init(
user: SwiftUI.Binding<User>,
isEditing: SwiftUI.Binding<Bool>,
showDetails: Bool,
@ViewBuilder content: () -> Content = { EmptyView() }
) {
self._user = user
self._isEditing = isEditing
self.showDetails = showDetails
self.content = content()
}
}JSON Usage:
{
"type": "UserProfile",
"user": "@{currentUser}", // Binding to viewModel.data.currentUser
"isEditing": "@{editMode}", // Binding to viewModel.data.editMode
"showDetails": true // Static value
}How Binding Properties Work:
-
At Generation Time: Properties prefixed with
@generate@SwiftUI.Bindingvariables -
In JSON: Use
@{propertyName}syntax to bind to ViewModel data properties -
Static Mode: Converter generates
$viewModel.data.propertyNamefor bindings - Dynamic Mode: Adapter creates proper SwiftUI bindings from ViewModel
Binding Use Cases:
- Two-way data binding for editable fields
- Shared state between parent and child components
- Reactive UI updates when data changes
- Form inputs that modify parent state
Explicitly marks the component as a container that can have child components.
sjui g converter CardStack --containerGenerated Swift signature:
struct CardStack<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
}Behavior:
- Forces the component to accept children even if no attributes are defined
- Generates a generic
Content: Viewtype parameter - Uses
@ViewBuilderfor SwiftUI DSL support - In JSON, children are specified via
"child"or"children"array
Explicitly marks the component as non-container (leaf component that cannot have children).
sjui g converter StatusBadge --no-containerGenerated Swift signature:
struct StatusBadge: View {
// No content parameter, only attribute parameters
}Behavior:
- Component will ignore any
"child"or"children"in JSON - No generic type parameter is generated
- Simpler implementation for components that don't need children
- Useful for atomic components like badges, icons, or status indicators
If neither --container nor --no-container is specified:
- The converter auto-detects based on presence of
"child"or"children"in JSON - Component is generated with optional content support
- This is the most flexible option but may have slightly more complex generated code
Disables the use of default attributes (advanced option, rarely needed).
sjui g converter MyComponent --no-default-attributesBehavior:
- By default, converters inherit common modifiers from BaseViewConverter
- This option creates a minimal converter without default modifier support
- Use only when you need complete control over all component behavior
Overwrites existing files without prompting for confirmation.
sjui g converter MyComponent --forceBehavior:
- Skips the "Overwrite? (y/n)" prompt for existing files
- Useful for scripting or when regenerating components
- Caution: Will overwrite any customizations in existing files
Location: Extensions/MyComponent.swift
Purpose: The actual SwiftUI View implementation
Example:
import SwiftUI
struct MyCard<Content: View>: View {
let title: String
let subtitle: String
let content: Content?
init(
title: String,
subtitle: String,
@ViewBuilder content: () -> Content = { EmptyView() }
) {
self.title = title
self.subtitle = subtitle
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
if let content = content {
content
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
}Customization: This file is meant to be edited! Modify the body to create your desired UI.
Location: sjui_tools/lib/swiftui/views/extensions/my_component_converter.rb
Purpose: Converts JSON to Swift code during Static mode compilation
Example:
class MyCardConverter < BaseViewConverter
def convert
is_container = (@component['children'] && !@component['children'].empty?) ||
(@component['child'] && !@component['child'].empty?)
params = []
if @component['title']
formatted_value = format_value(@component['title'], 'String')
params << "title: #{formatted_value}" if formatted_value
end
if @component['subtitle']
formatted_value = format_value(@component['subtitle'], 'String')
params << "subtitle: #{formatted_value}" if formatted_value
end
if is_container
add_line "MyCard("
indent do
params.each_with_index do |param, index|
add_line index == params.length - 1 ? param : "#{param},"
end
end
add_line ") {"
indent do
process_children
end
add_line "}"
else
# Non-container implementation
add_line "MyCard("
indent do
params.each_with_index do |param, index|
add_line index == params.length - 1 ? param : "#{param},"
end
end
add_line ")"
end
apply_modifiers
generated_code
end
endCustomization: You can modify this to handle special JSON attributes or add custom logic.
Location: Extensions/Adapters/MyComponentAdapter.swift
Purpose: Enables Dynamic mode support with hot reload
Example:
#if DEBUG
import SwiftUI
import SwiftJsonUI
struct MyCardAdapter: CustomComponentAdapter {
var componentType: String { "MyCard" }
func buildView(
component: DynamicComponent,
viewModel: DynamicViewModel,
viewId: String?,
parentOrientation: String?
) -> AnyView {
// Extract attributes from raw JSON data
let title = component.rawData["title"] as? String ?? ""
let subtitle = component.rawData["subtitle"] as? String ?? ""
// Build content from children
let content = VStack(alignment: .leading, spacing: 0) {
if let children = component.childComponents {
ForEach(Array(children.enumerated()), id: \.offset) { _, child in
DynamicComponentBuilder(
component: child,
viewModel: viewModel,
viewId: viewId,
isWeightedChild: false,
parentOrientation: "vertical"
)
}
}
}
return AnyView(
MyCard(
title: title,
subtitle: subtitle
) {
content
}
.modifier(CommonModifiers(component: component, viewModel: viewModel))
)
}
}
#endifNote: This is only compiled in DEBUG builds for optimal release performance.
sjui g converter NotificationBadge --attributes "count:Int,color:Color" --no-containerJSON Usage:
{
"type": "NotificationBadge",
"count": 5,
"color": "#FF0000"
}sjui g converter FeatureCard --attributes "icon:String,title:String,description:String"JSON Usage:
{
"type": "FeatureCard",
"icon": "star.fill",
"title": "Premium Feature",
"description": "Unlock advanced capabilities",
"child": [
{
"type": "Button",
"text": "Learn More",
"onclick": "showDetails"
}
]
}sjui g converter Dashboard --attributes "headerTitle:String,showStats:Bool" --containerJSON Usage:
{
"type": "Dashboard",
"headerTitle": "Analytics",
"showStats": true,
"child": [
{
"type": "View",
"orientation": "horizontal",
"child": [
{ "type": "Label", "text": "Total Users: 1,234" },
{ "type": "Label", "text": "Active: 567" }
]
},
{
"type": "MyChart",
"data": "@{chartData}"
}
]
}Add to sjui.config.json:
{
"extension_directory": "Extensions",
"adapter_directory": "Extensions/Adapters"
}The generated components are automatically registered, but ensure you call this in your app initialization:
#if DEBUG
// In App.swift or AppDelegate
CustomComponentRegistration.registerAll()
#endifAutomatically updated with component mappings:
CONVERTER_MAPPINGS = {
'MyCard' => 'MyCardConverter',
'MyButton' => 'MyButtonConverter',
# ... automatically maintained
}.freezeAutomatically updated with adapter registration:
let adapters: [CustomComponentAdapter] = [
MyCardAdapter(),
MyButtonAdapter(),
// ... automatically maintained
]- Use PascalCase for component names
- Be descriptive:
UserProfileCardnotCard1 - Avoid iOS system component names
- Keep attributes simple and focused
- Use appropriate types (Bool for flags, String for text)
- Consider default values in Swift implementation
- Default to container if unsure (more flexible)
- Use non-container for leaf components (badges, icons)
- Container components should define clear content areas
- Generate the component
- Test with basic JSON
- Customize the Swift file for appearance
- Adjust converter if needed for special attributes
- Test in both Static and Dynamic modes
- Keep adapter implementations lightweight
- Heavy logic belongs in the Swift component
- Use
#if DEBUGappropriately
Error: "Unknown component type: MyComponent"
Solution:
- Ensure converter_mappings.rb includes your component
- Run
sjui build --cleanto regenerate - Check that files were generated in correct locations
Error: Component appears as unknown in Dynamic mode
Solution:
- Verify adapter file exists in adapter_directory
- Check CustomComponentRegistration.swift includes adapter
- Ensure
CustomComponentRegistration.registerAll()is called - Confirm you're in DEBUG build
Issue: Custom attribute in JSON not affecting component
For Static Mode:
- Check converter .rb file processes the attribute
- Verify format_value handles the type correctly
For Dynamic Mode:
- Check adapter accesses attribute via component.rawData
- Ensure attribute name matches exactly (case-sensitive)
Error: Swift compilation errors after generation
Solution:
- Check generated Swift syntax is valid
- Ensure all types in attributes are supported
- Look for typos in attribute definitions
- Verify import statements are present
For custom types beyond the basics:
- Define the type in Swift:
struct MyCustomType {
let value: String
}- Handle in converter:
def format_value(value, type)
case type
when 'MyCustomType'
# Custom parsing logic
"MyCustomType(value: \"#{value}\")"
else
super
end
end- Parse in adapter:
let customValue = component.rawData["customField"] as? [String: Any]
let myType = MyCustomType(value: customValue?["value"] as? String ?? "")Handle conditional child rendering:
def process_children
child_array = @component['child'] || []
# Filter based on conditions
child_array.each do |child|
next if child['platform'] && child['platform'] != 'ios'
child_converter = @factory.create_converter(child, @indent_level, @action_manager, @factory, @registry)
@generated_code.concat(child_converter.convert.split("\n"))
end
endFor stateful components, use @State in Swift:
struct MyComponent: View {
let initialValue: Int
@State private var currentValue: Int
init(initialValue: Int) {
self.initialValue = initialValue
self._currentValue = State(initialValue: initialValue)
}
}- SwiftJsonUI-7.1.0-Release-Notes - Introduction to custom components
- CLI-Commands - Complete CLI reference
- Advanced-Features - Advanced SwiftJsonUI features
- Data-Binding - Using data binding with custom components