Converter Command - Tai-Kimura/SwiftJsonUI GitHub Wiki

Converter Command (sjui g converter)

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.

Table of Contents

Overview

The converter generator creates three essential files that work together:

  1. Swift Component - The actual SwiftUI View
  2. Ruby Converter - Converts JSON to Swift code for Static mode
  3. Dynamic Adapter - Enables Dynamic mode with hot reload

This is the primary way to extend SwiftJsonUI with custom, reusable components.

Basic Usage

Simple Component

sjui g converter MyButton

Component with Attributes

sjui g converter MyCard --attributes "title:String,subtitle:String,imageUrl:String"

Container Component

sjui g converter MyContainer --container

Non-Container Component

sjui g converter MyBadge --no-container

Command Options

--attributes (-a)

Defines 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:Color

How 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)

Binding Properties (New in 7.1.1)

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:

  1. At Generation Time: Properties prefixed with @ generate @SwiftUI.Binding variables
  2. In JSON: Use @{propertyName} syntax to bind to ViewModel data properties
  3. Static Mode: Converter generates $viewModel.data.propertyName for bindings
  4. 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

--container

Explicitly marks the component as a container that can have child components.

sjui g converter CardStack --container

Generated 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: View type parameter
  • Uses @ViewBuilder for SwiftUI DSL support
  • In JSON, children are specified via "child" or "children" array

--no-container

Explicitly marks the component as non-container (leaf component that cannot have children).

sjui g converter StatusBadge --no-container

Generated 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

Auto-detection (default)

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

--no-default-attributes

Disables the use of default attributes (advanced option, rarely needed).

sjui g converter MyComponent --no-default-attributes

Behavior:

  • 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

--force (-f)

Overwrites existing files without prompting for confirmation.

sjui g converter MyComponent --force

Behavior:

  • Skips the "Overwrite? (y/n)" prompt for existing files
  • Useful for scripting or when regenerating components
  • Caution: Will overwrite any customizations in existing files

Generated Files

1. Swift Component File

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.

2. Ruby Converter File

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
end

Customization: You can modify this to handle special JSON attributes or add custom logic.

3. Dynamic Adapter File

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))
        )
    }
}

#endif

Note: This is only compiled in DEBUG builds for optimal release performance.

Examples

Example 1: Simple Badge Component

sjui g converter NotificationBadge --attributes "count:Int,color:Color" --no-container

JSON Usage:

{
  "type": "NotificationBadge",
  "count": 5,
  "color": "#FF0000"
}

Example 2: Custom Card with Children

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"
    }
  ]
}

Example 3: Complex Container

sjui g converter Dashboard --attributes "headerTitle:String,showStats:Bool" --container

JSON 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}"
    }
  ]
}

Configuration

Project Configuration

Add to sjui.config.json:

{
  "extension_directory": "Extensions",
  "adapter_directory": "Extensions/Adapters"
}

Registration

The generated components are automatically registered, but ensure you call this in your app initialization:

#if DEBUG
// In App.swift or AppDelegate
CustomComponentRegistration.registerAll()
#endif

Auto-Generated Files

converter_mappings.rb

Automatically updated with component mappings:

CONVERTER_MAPPINGS = {
  'MyCard' => 'MyCardConverter',
  'MyButton' => 'MyButtonConverter',
  # ... automatically maintained
}.freeze

CustomComponentRegistration.swift

Automatically updated with adapter registration:

let adapters: [CustomComponentAdapter] = [
    MyCardAdapter(),
    MyButtonAdapter(),
    // ... automatically maintained
]

Best Practices

1. Naming Conventions

  • Use PascalCase for component names
  • Be descriptive: UserProfileCard not Card1
  • Avoid iOS system component names

2. Attribute Design

  • Keep attributes simple and focused
  • Use appropriate types (Bool for flags, String for text)
  • Consider default values in Swift implementation

3. Container vs Non-Container

  • Default to container if unsure (more flexible)
  • Use non-container for leaf components (badges, icons)
  • Container components should define clear content areas

4. Customization Workflow

  1. Generate the component
  2. Test with basic JSON
  3. Customize the Swift file for appearance
  4. Adjust converter if needed for special attributes
  5. Test in both Static and Dynamic modes

5. Performance

  • Keep adapter implementations lightweight
  • Heavy logic belongs in the Swift component
  • Use #if DEBUG appropriately

Troubleshooting

Component Not Found

Error: "Unknown component type: MyComponent"

Solution:

  • Ensure converter_mappings.rb includes your component
  • Run sjui build --clean to regenerate
  • Check that files were generated in correct locations

Dynamic Mode Not Working

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

Attribute Not Appearing

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)

Build Errors

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

Advanced Topics

Custom Type Handling

For custom types beyond the basics:

  1. Define the type in Swift:
struct MyCustomType {
    let value: String
}
  1. Handle in converter:
def format_value(value, type)
  case type
  when 'MyCustomType'
    # Custom parsing logic
    "MyCustomType(value: \"#{value}\")"
  else
    super
  end
end
  1. Parse in adapter:
let customValue = component.rawData["customField"] as? [String: Any]
let myType = MyCustomType(value: customValue?["value"] as? String ?? "")

Conditional Children

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
end

State Management

For 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)
    }
}

See Also

⚠️ **GitHub.com Fallback** ⚠️