Data Binding - Tai-Kimura/SwiftJsonUI GitHub Wiki

Data Binding in SwiftJsonUI

Data binding in SwiftJsonUI provides powerful reactive UI capabilities, allowing views to automatically update when data changes.

Table of Contents

Basic Data Binding

Simple Property Binding

Use the @{} syntax to bind view properties to data variables:

{
  "type": "Label",
  "text": "@{userName}",
  "visibility": "@{isLoggedIn ? visible : gone}"
}

Data Declaration

Define data models using the data array at the root level:

{
  "data": [
    {
      "name": "userName",
      "class": "String",
      "defaultValue": "\"Guest\""
    },
    {
      "name": "isLoggedIn",
      "class": "Bool",
      "defaultValue": "false"
    },
    {
      "name": "itemCount",
      "class": "Int",
      "defaultValue": "0"
    },
    {
      "name": "user",
      "class": "User"
    }
  ],
  "child": [...]
}

Data Properties

Property Type Description Required
name string Variable name Yes
class string Swift type (String, Bool, Int, Float, custom classes) Yes
defaultValue string Swift code for initial value (see details below) No

Understanding defaultValue

The defaultValue property must be a string containing valid Swift code that will be written directly into the generated Swift file. This is NOT a JSON value.

Examples by Type:

Data Type JSON Definition Generated Swift Code
String "defaultValue": "\"Hello\"" var name: String = "Hello"
Empty String "defaultValue": "\"\"" var name: String = ""
Bool "defaultValue": "true" var flag: Bool = true
Int "defaultValue": "42" var count: Int = 42
Float "defaultValue": "3.14" var value: Float = 3.14
Array "defaultValue": "[1, 2, 3]" var items: Array = [1, 2, 3]
Dictionary "defaultValue": "[\"key\": \"value\"]" var dict: Dictionary = ["key": "value"]
Optional "defaultValue": "nil" var user: User? = nil
Custom Object "defaultValue": "User(name: \"John\")" var user: User = User(name: "John")

Important Notes:

  • String values require escaped quotes: "\"text\""
  • The content is written as-is into Swift code
  • Must be valid Swift syntax, not JSON
  • Can include Swift expressions: "Date()"
  • Can call initializers: "CGRect(x: 0, y: 0, width: 100, height: 50)"

Supported Data Types

  • Primitives: String, Bool, Int, Float, Double
  • Collections: Array, Dictionary, Set
  • Custom Classes: Any Swift class name
  • Optionals: Indicated by ? in expressions
  • Foundation Types: Date, URL, Data, etc.

Binding Expressions

Expression Syntax

SwiftJsonUI supports Swift-like expressions within @{}:

Optional Chaining

{
  "text": "@{user?.fullName}",
  "visibility": "@{profile?.isPublic ? visible : gone}"
}

Ternary Operators

{
  "background": "@{isSelected ? #007AFF : #FFFFFF}",
  "text": "@{count > 0 ? '\(count) items' : 'No items'}"
}

Method Calls

{
  "text": "@{message.localized()}",
  "fontColor": "@{theme.textColor()}"
}

Complex Expressions

{
  "visibility": "@{items.count > 0 && isEnabled ? visible : gone}",
  "text": "@{String(format: '%.2f', price)}",
  "enabled": "@{user.isVerified && !isLoading}"
}

String Interpolation

{
  "text": "@{'Welcome, ' + userName}",
  "hint": "@{'\(remainingCount) characters remaining'}"
}

Common Binding Patterns

Visibility Control

{
  "visibility": "@{hasData ? visible : gone}",
  "hidden": "@{!isReady}"
}

Conditional Styling

{
  "background": "@{isError ? #FF0000 : #FFFFFF}",
  "fontColor": "@{isDisabled ? #999999 : #000000}"
}

Dynamic Text

{
  "text": "@{isLoading ? 'Loading...' : resultText}",
  "hint": "@{fieldType == 'email' ? 'Enter email' : 'Enter text'}"
}

Binding Groups

Group related bindings for coordinated updates:

{
  "data": [
    {
      "name": "formData",
      "class": "FormModel"
    }
  ],
  "child": [
    {
      "type": "View",
      "binding_group": "form_fields",
      "child": [
        {
          "type": "TextField",
          "id": "email_field",
          "text": "@{formData.email}",
          "binding_group": "form_fields"
        },
        {
          "type": "TextField",
          "id": "password_field",
          "text": "@{formData.password}",
          "binding_group": "form_fields"
        },
        {
          "type": "Button",
          "text": "Submit",
          "enabled": "@{formData.isValid}",
          "binding_group": "form_fields"
        }
      ]
    }
  ]
}

Benefits of Binding Groups

  • Performance: Batch updates for multiple related views
  • Consistency: Ensure related UI elements update together
  • Organization: Logical grouping of related bindings

Custom Binding Scripts

For complex binding logic, use bindingScript:

{
  "type": "View",
  "id": "custom_view",
  "bindingScript": "customView.backgroundColor = user.isPremium ? .systemBlue : .systemGray\ncustomView.layer.borderWidth = user.isPremium ? 2.0 : 0.0",
  "child": [...]
}

BindingScript Features

  • Direct Swift code execution
  • Access to view properties via view ID
  • Complex calculations and conditionals
  • Multiple property updates

Example: Complex State Management

{
  "type": "Label",
  "id": "status_label",
  "bindingScript": "switch orderStatus {\ncase .pending:\n  statusLabel.text = 'Processing...'\n  statusLabel.textColor = .orange\ncase .completed:\n  statusLabel.text = 'Completed'\n  statusLabel.textColor = .green\ncase .failed:\n  statusLabel.text = 'Failed'\n  statusLabel.textColor = .red\ndefault:\n  statusLabel.text = 'Unknown'\n  statusLabel.textColor = .gray\n}"
}

Include System Variables

Passing Data to Includes

Use shared_data to pass data to included components:

{
  "type": "View",
  "child": [
    {
      "include": "user_card.json",
      "binding_id": "user_card_1",
      "shared_data": {
        "user": "@{currentUser}",
        "showDetails": true
      }
    }
  ]
}

Receiving Data in Includes

In the included file (user_card.json):

{
  "variables": [
    {
      "name": "user",
      "class": "User"
    },
    {
      "name": "showDetails",
      "class": "Bool",
      "defaultValue": false
    }
  ],
  "type": "View",
  "child": [
    {
      "type": "Label",
      "text": "@{user.name}"
    },
    {
      "type": "Label",
      "text": "@{user.email}",
      "visibility": "@{showDetails ? visible : gone}"
    }
  ]
}

Variable Properties

Property Type Description Required
name string Variable name Yes
class string Variable type Yes
defaultValue any Default value if not provided No

Advanced Binding Techniques

Two-Way Binding

For input fields, binding automatically works both ways:

{
  "type": "TextField",
  "text": "@{userInput}",
  "onTextChange": "updateModel"
}

Collection Binding

Bind to array elements:

{
  "text": "@{items[selectedIndex].title}",
  "visibility": "@{items.count > selectedIndex ? visible : gone}"
}

Computed Properties

Use expressions for computed values:

{
  "text": "@{firstName + ' ' + lastName}",
  "hint": "@{'Total: $' + String(format: '%.2f', price * quantity)}"
}

Null Safety

Handle optionals safely:

{
  "text": "@{user?.name ?? 'Anonymous'}",
  "visibility": "@{data != nil ? visible : gone}"
}

Best Practices

  1. Use Binding Groups for related UI elements
  2. Leverage Optional Chaining for null safety
  3. Keep Expressions Simple - move complex logic to bindingScript
  4. Use Variables for reusable components
  5. Provide Default Values for better initialization
  6. Test Edge Cases with nil/empty data

Performance Considerations

  • Binding groups improve performance by batching updates
  • Complex expressions are evaluated on every change
  • Use bindingScript for heavy computations
  • Minimize nested optional chaining

Troubleshooting

Common Issues

  1. Binding not updating: Check binding_group assignment
  2. Nil crashes: Use optional chaining (?.)
  3. Type mismatches: Ensure data class matches usage
  4. Performance issues: Simplify expressions or use bindingScript

Debug Tips

  • Log binding values in bindingScript
  • Use simple test data first
  • Verify data declaration syntax
  • Check for typos in variable names

Examples

Login Form

{
  "data": [
    {
      "name": "email",
      "class": "String",
      "defaultValue": "\"\""
    },
    {
      "name": "password", 
      "class": "String",
      "defaultValue": "\"\""
    },
    {
      "name": "isValid",
      "class": "Bool",
      "defaultValue": "false"
    }
  ],
  "child": [
    {
      "type": "TextField",
      "hint": "Email",
      "text": "@{email}",
      "input": "email"
    },
    {
      "type": "TextField",
      "hint": "Password",
      "text": "@{password}",
      "input": "password",
      "secure": true
    },
    {
      "type": "Button",
      "text": "Login",
      "enabled": "@{email.count > 0 && password.count >= 8}"
    }
  ]
}

Dynamic List Item

{
  "variables": [
    {
      "name": "item",
      "class": "Product"
    },
    {
      "name": "isInCart",
      "class": "Bool"
    }
  ],
  "type": "View",
  "child": [
    {
      "type": "Label",
      "text": "@{item.name}"
    },
    {
      "type": "Label",
      "text": "@{'$' + String(item.price)}",
      "fontColor": "@{item.onSale ? #FF0000 : #000000}"
    },
    {
      "type": "Button",
      "text": "@{isInCart ? 'Remove' : 'Add to Cart'}",
      "background": "@{isInCart ? #FF0000 : #007AFF}"
    }
  ]
}

See Also