Advanced Features - Tai-Kimura/SwiftJsonUI GitHub Wiki

Advanced Features in SwiftJsonUI

This document covers advanced features and techniques in SwiftJsonUI for building sophisticated, dynamic user interfaces.

Table of Contents

Shadow Configuration

Shadow Object Structure

The shadow attribute accepts both string and object formats for advanced shadow effects:

Simple Shadow (String)

{
  "type": "View",
  "shadow": "#000000"
}

Advanced Shadow (Object)

{
  "type": "View",
  "shadow": {
    "color": "#000000",
    "offsetX": 0,
    "offsetY": 2,
    "opacity": 0.3,
    "radius": 4
  }
}

Shadow Properties

Property Type Description Default
color string Shadow color (hex) #000000
offsetX float Horizontal offset 0
offsetY float Vertical offset 2
opacity float Shadow opacity (0.0-1.0) 0.2
radius float Blur radius 3

Advanced Shadow Examples

Card with Elevation

{
  "type": "View",
  "background": "#FFFFFF",
  "cornerRadius": 8,
  "shadow": {
    "color": "#000000",
    "offsetY": 4,
    "opacity": 0.15,
    "radius": 8
  }
}

Dynamic Shadow

{
  "type": "View",
  "shadow": {
    "color": "@{isSelected ? #007AFF : #000000}",
    "opacity": "@{isSelected ? 0.5 : 0.2}",
    "offsetY": "@{isPressed ? 1 : 4}",
    "radius": "@{isPressed ? 2 : 6}"
  }
}

Custom Binding Scripts

Overview

bindingScript allows direct Swift code execution for complex binding logic that can't be expressed with simple expressions.

Basic Usage

{
  "type": "View",
  "id": "custom_view",
  "bindingScript": "customView.alpha = isVisible ? 1.0 : 0.5"
}

Multi-line Scripts

{
  "type": "Label",
  "id": "status_label",
  "bindingScript": "let color: UIColor\nswitch status {\ncase .active:\n  color = .green\ncase .pending:\n  color = .orange\ncase .inactive:\n  color = .red\ndefault:\n  color = .gray\n}\nstatusLabel.textColor = color\nstatusLabel.text = status.description"
}

Accessing Multiple Views

{
  "bindingScript": "if let button = self.viewWithTag(100) as? UIButton {\n  button.isEnabled = formIsValid\n}\nif let label = self.viewWithTag(101) as? UILabel {\n  label.text = formIsValid ? 'Ready' : 'Complete all fields'\n}"
}

Animation in Binding Scripts

{
  "bindingScript": "UIView.animate(withDuration: 0.3) {\n  customView.alpha = isVisible ? 1.0 : 0.0\n  customView.transform = isExpanded ? .identity : CGAffineTransform(scaleX: 0.8, y: 0.8)\n}"
}

Touch Handling and Gestures

Touch Disabled States

Control touch propagation with touchDisabledState:

{
  "type": "View",
  "touchDisabledState": "viewsWithoutTouchEnabled",
  "touchEnabledViewIds": ["button1", "button2"],
  "child": [
    {
      "type": "Button",
      "id": "button1",
      "text": "Enabled"
    },
    {
      "type": "Button",
      "id": "button2",
      "text": "Also Enabled"
    },
    {
      "type": "Button",
      "id": "button3",
      "text": "Disabled by parent"
    }
  ]
}

Touch Disabled State Options

Value Description
none Normal touch handling
onlyMe Disable touch only for this view
viewsWithoutTouchEnabled Disable all except specified views
viewsWithoutInList Disable views not in the enabled list

Can Tap Property

Enable tap handling for non-button views:

{
  "type": "View",
  "canTap": true,
  "onclick": "handleViewTap",
  "tapBackground": "#F0F0F0"
}

Complex Event Handling

Define multiple event types with the events dictionary:

{
  "type": "View",
  "events": {
    "onclick": "handleTap",
    "onlongtap": "handleLongPress",
    "pan": {
      "selector": "handlePan:",
      "minimumDistance": 10,
      "maximumDistance": 100
    },
    "swipe": {
      "selector": "handleSwipe:",
      "direction": "left"
    },
    "rotation": {
      "selector": "handleRotation:"
    }
  }
}

Dynamic Image Loading

Source Name Binding

Use srcName for dynamic image selection:

{
  "type": "Image",
  "srcName": "@{iconType + '_icon'}",
  "contentMode": "AspectFit"
}

Conditional Image Sources

{
  "type": "Image",
  "src": "@{isSelected ? 'checked' : 'unchecked'}",
  "highlightSrc": "@{isSelected ? 'checked_highlight' : 'unchecked_highlight'}"
}

Network Image with URL Binding

{
  "type": "NetworkImage",
  "url": "@{user.profileImageUrl}",
  "defaultImage": "default_avatar",
  "errorImage": "error_image",
  "loadingImage": "loading_spinner"
}

Advanced Layout Techniques

Direction-based Layouts

Control layout flow direction:

{
  "type": "View",
  "orientation": "vertical",
  "direction": "bottomToTop",
  "child": [
    {
      "type": "Label",
      "text": "This appears at bottom"
    },
    {
      "type": "Label",
      "text": "This appears at top"
    }
  ]
}

Direction Options

Value Description
topToBottom Default vertical flow
bottomToTop Reverse vertical flow
leftToRight Default horizontal flow
rightToLeft Reverse horizontal flow (RTL)
none No automatic layout

Individual Padding Control

Fine-grained padding control:

{
  "type": "View",
  "leftPadding": 20,
  "rightPadding": 20,
  "topPadding": 10,
  "bottomPadding": 30
}

Container Insets for Text

Control text container insets:

{
  "type": "TextView",
  "containerInset": [8, 12, 8, 12],
  "text": "Text with custom insets"
}

Collection View Configuration

Advanced Cell Configuration

{
  "type": "Collection",
  "cellClasses": [
    {
      "className": "HeaderCell"
    },
    {
      "className": "ContentCell"
    },
    {
      "className": "FooterCell"
    }
  ],
  "headerClasses": [
    {
      "className": "SectionHeader"
    }
  ],
  "footerClasses": [
    {
      "className": "SectionFooter"
    }
  ],
  "insetHorizontal": 16,
  "insetVertical": 8,
  "horizontalScroll": true,
  "setTargetAsDelegate": true,
  "setTargetAsDataSource": true
}

Note: cellClasses, headerClasses, and footerClasses only accept className property. The actual cell layouts are defined separately and registered with these class names.

Dynamic Cell Class Names

{
  "type": "Collection",
  "cellClasses": [
    {
      "className": "@{cellType}Cell"
    }
  ]
}

This allows dynamic cell type selection based on data.

Complex Event Handling

onclick vs onClick - Different Systems

onclick (Selector-based)

onclick calls Objective-C selectors or ViewModel methods:

UIKit Mode:

{
  "type": "Button",
  "onclick": "handleButtonTap"  // Calls @objc func handleButtonTap()
}

To receive the sender as parameter, add colon:

{
  "type": "Button",
  "onclick": "handleButtonTap:"  // Calls @objc func handleButtonTap(_ sender: UIButton)
}

SwiftUI Mode:

{
  "type": "Button",
  "onclick": "handleButtonTap"  // Calls viewModel.handleButtonTap()
}

onClick (Closure-based)

onClick uses Swift closures defined in data:

{
  "data": [
    {
      "name": "tapHandler",
      "class": "((UITapGestureRecognizer) -> Void)",
      "defaultValue": "{ _ in print(\"Tapped!\") }"
    }
  ],
  "type": "View",
  "onClick": "@{tapHandler}"
}

Gesture Recognizers with Closures

SwiftJsonUI supports various gesture recognizers with closure handlers:

Long Press

{
  "data": [
    {
      "name": "longPressHandler",
      "class": "((UILongPressGestureRecognizer) -> Void)",
      "defaultValue": "{ gesture in\n  if gesture.state == .began {\n    print(\"Long press started\")\n  }\n}"
    }
  ],
  "type": "View",
  "onLongPress": "@{longPressHandler}"
}

Pinch Gesture

{
  "data": [
    {
      "name": "pinchHandler",
      "class": "((UIPinchGestureRecognizer) -> Void)",
      "defaultValue": "{ gesture in\n  let scale = gesture.scale\n  print(\"Pinch scale: \\(scale)\")\n}"
    }
  ],
  "type": "View",
  "onPinch": "@{pinchHandler}"
}

Pan Gesture

{
  "data": [
    {
      "name": "panHandler",
      "class": "((UIPanGestureRecognizer) -> Void)",
      "defaultValue": "{ gesture in\n  let translation = gesture.translation(in: gesture.view)\n  print(\"Pan: \\(translation)\")\n}"
    }
  ],
  "type": "View",
  "onPan": "@{panHandler}"
}

Multiple Event Types on Buttons

For buttons with multiple touch events:

{
  "type": "Button",
  "onclick": [
    {
      "event": "touchDown",
      "selector": "buttonPressed:"
    },
    {
      "event": "touchUpInside",
      "selector": "buttonReleased:"
    },
    {
      "event": "touchDragExit",
      "selector": "buttonDraggedOut:"
    }
  ]
}

Performance Optimization

Binding Groups for Batch Updates

{
  "binding_group": "form_validation",
  "child": [
    {
      "type": "TextField",
      "binding_group": "form_validation",
      "text": "@{email}"
    },
    {
      "type": "Label",
      "binding_group": "form_validation",
      "text": "@{emailError}",
      "visibility": "@{emailError != nil ? visible : gone}"
    }
  ]
}

Lazy Loading with Includes

{
  "type": "View",
  "child": [
    {
      "type": "View",
      "visibility": "@{shouldLoadContent ? visible : gone}",
      "child": [
        {
          "include": "@{shouldLoadContent ? 'heavy_content.json' : 'placeholder.json'}"
        }
      ]
    }
  ]
}

Advanced SafeArea Configuration

Partial SafeArea Insets

{
  "type": "SafeAreaView",
  "safeAreaInsetPositions": ["top", "bottom"],
  "child": [
    {
      "type": "View",
      "background": "#007AFF",
      "child": [
        {
          "type": "Label",
          "text": "Content with top and bottom safe area only"
        }
      ]
    }
  ]
}

Vertical SafeArea Shorthand

{
  "type": "SafeAreaView",
  "safeAreaInsetPositions": ["vertical"],  // Applies to top and bottom
  "child": [...]
}

Debugging Techniques

Using Tags for View Identification

{
  "type": "View",
  "tag": 100,
  "bindingScript": "if let view = self.viewWithTag(100) {\n  print('Found view: \\(view)')\n}"
}

Conditional Debugging

{
  "bindingScript": "#if DEBUG\nprint('Current state: \\(currentState)')\nprint('User data: \\(userData)')\n#endif"
}

Complex Examples

Animated Expandable Card

{
  "data": [
    {
      "name": "isExpanded",
      "class": "Bool",
      "defaultValue": false
    }
  ],
  "type": "View",
  "id": "card",
  "background": "#FFFFFF",
  "cornerRadius": 12,
  "shadow": {
    "color": "#000000",
    "offsetY": "@{isExpanded ? 8 : 2}",
    "opacity": "@{isExpanded ? 0.2 : 0.1}",
    "radius": "@{isExpanded ? 12 : 4}"
  },
  "canTap": true,
  "onclick": "toggleExpanded",
  "bindingScript": "UIView.animate(withDuration: 0.3) {\n  card.transform = self.isExpanded ? CGAffineTransform(scaleX: 1.05, y: 1.05) : .identity\n}",
  "child": [
    {
      "type": "Label",
      "text": "Tap to expand"
    },
    {
      "type": "View",
      "height": "@{isExpanded ? 200 : 0}",
      "clipToBounds": true,
      "child": [
        {
          "type": "Label",
          "text": "Expanded content here"
        }
      ]
    }
  ]
}

Dynamic Form with Validation

{
  "data": [
    {
      "name": "formData",
      "class": "FormModel"
    },
    {
      "name": "errors",
      "class": "Dictionary"
    }
  ],
  "type": "View",
  "binding_group": "form",
  "bindingScript": "let isValid = formData.email.contains('@') && formData.password.count >= 8\nsubmitButton.isEnabled = isValid\nsubmitButton.alpha = isValid ? 1.0 : 0.5",
  "child": [
    {
      "type": "TextField",
      "id": "emailField",
      "binding_group": "form",
      "text": "@{formData.email}",
      "borderColor": "@{errors['email'] != nil ? #FF0000 : #CCCCCC}"
    },
    {
      "type": "Label",
      "binding_group": "form",
      "text": "@{errors['email']}",
      "fontColor": "#FF0000",
      "visibility": "@{errors['email'] != nil ? visible : gone}"
    },
    {
      "type": "Button",
      "id": "submitButton",
      "binding_group": "form",
      "text": "Submit"
    }
  ]
}

Best Practices

  1. Use binding groups for related UI updates
  2. Prefer expressions over bindingScript when possible
  3. Cache complex calculations in data models
  4. Test on various devices for SafeArea behavior
  5. Use meaningful IDs and tags for debugging
  6. Document complex bindingScripts with comments
  7. Avoid deep nesting in shadow configurations
  8. Optimize images for dynamic loading

See Also