Custom Components for Swift

This guide explains how to configure custom Swift UI components in Locofy using the locofy.config.json file. This allows you to map your existing SwiftUI views to Figma designs, enabling UIPro to generate iOS code using your actual component library.

Configuration File: locofy.config.json

The locofy.config.json file defines how your Swift UI components map to Figma components. It's automatically generated by UIPro when you run: "Create UIPro config for all components"

File Location

YourApp/
├── YourApp/
│   └── Components/
├── locofy.config.json  ← Configuration file
└── YourApp.xcodeproj

SwiftUI Component Examples

Example 1: Button Component

Component Code:

import SwiftUI
 
struct CustomButton: View {
    let label: String
    var size: ButtonSize = .medium
    var variant: ButtonVariant = .primary
    var disabled: Bool = false
    var action: () -> Void = {}
    
    enum ButtonSize {
        case small, medium, large
        
        var padding: EdgeInsets {
            switch self {
            case .small: return EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12)
            case .medium: return EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16)
            case .large: return EdgeInsets(top: 14, leading: 24, bottom: 14, trailing: 24)
            }
        }
        
        var fontSize: CGFloat {
            switch self {
            case .small: return 14
            case .medium: return 16
            case .large: return 18
            }
        }
    }
    
    enum ButtonVariant {
        case primary, secondary
        
        var backgroundColor: Color {
            switch self {
            case .primary: return Color.blue
            case .secondary: return Color.purple
            }
        }
    }
    
    var body: some View {
        Button(action: action) {
            Text(label)
                .font(.system(size: size.fontSize, weight: .semibold))
                .foregroundColor(.white)
                .padding(size.padding)
                .background(variant.backgroundColor)
                .cornerRadius(8)
        }
        .disabled(disabled)
        .opacity(disabled ? 0.5 : 1.0)
    }
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./YourApp/Components/CustomButton.swift",
      "name": "CustomButton",
      "props": [
        {
          "name": "label",
          "dataType": "string",
          "propType": 1,
          "isOptional": false,
          "config": {
            "layerName": "[children]"
          }
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["small", "medium", "large"],
          "config": {
            "layerProp": "Size"
          }
        },
        {
          "name": "variant",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["primary", "secondary"],
          "config": {
            "layerProp": "Variant"
          }
        },
        {
          "name": "disabled",
          "dataType": "boolean",
          "propType": 4,
          "isOptional": true
        }
      ],
      "config": {
        "layerName": "Button"
      }
    }
  ],
  "projectId": "your-project-id",
  "projectName": "Your iOS App"
}

Example 2: Card Component

Component Code:

import SwiftUI
 
struct CardView<Header: View, Content: View, Footer: View>: View {
    let header: Header?
    let content: Content
    let footer: Footer?
    
    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder header: () -> Header? = { nil },
        @ViewBuilder footer: () -> Footer? = { nil }
    ) {
        self.content = content()
        self.header = header()
        self.footer = footer()
    }
    
    var body: some View {
        VStack(spacing: 0) {
            if let header = header {
                header
                    .padding()
                Divider()
            }
            
            content
                .padding()
            
            if let footer = footer {
                Divider()
                footer
                    .padding()
            }
        }
        .background(Color.white)
        .cornerRadius(12)
        .shadow(color: Color.black.opacity(0.1), radius: 8, x: 0, y: 2)
    }
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./YourApp/Components/CardView.swift",
      "name": "CardView",
      "props": [
        {
          "name": "header",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "Header"
          }
        },
        {
          "name": "content",
          "dataType": "node",
          "propType": 5,
          "isOptional": false,
          "config": {
            "layerName": "Content"
          }
        },
        {
          "name": "footer",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "Footer"
          }
        }
      ],
      "config": {
        "layerName": "Card"
      }
    }
  ]
}

Example 3: TextField Component

Component Code:

import SwiftUI
 
struct CustomTextField: View {
    @Binding var text: String
    var placeholder: String = ""
    var label: String?
    var error: String?
    var isSecure: Bool = false
    var keyboardType: UIKeyboardType = .default
    var size: TextFieldSize = .medium
    
    enum TextFieldSize {
        case small, medium, large
        
        var padding: CGFloat {
            switch self {
            case .small: return 8
            case .medium: return 12
            case .large: return 16
            }
        }
        
        var fontSize: CGFloat {
            switch self {
            case .small: return 14
            case .medium: return 16
            case .large: return 18
            }
        }
    }
    
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            if let label = label {
                Text(label)
                    .font(.system(size: 14, weight: .semibold))
                    .foregroundColor(.primary)
            }
            
            Group {
                if isSecure {
                    SecureField(placeholder, text: $text)
                } else {
                    TextField(placeholder, text: $text)
                }
            }
            .keyboardType(keyboardType)
            .font(.system(size: size.fontSize))
            .padding(size.padding)
            .background(Color(.systemGray6))
            .cornerRadius(8)
            .overlay(
                RoundedRectangle(cornerRadius: 8)
                    .stroke(error != nil ? Color.red : Color.clear, lineWidth: 1)
            )
            
            if let error = error {
                Text(error)
                    .font(.system(size: 12))
                    .foregroundColor(.red)
            }
        }
    }
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./YourApp/Components/CustomTextField.swift",
      "name": "CustomTextField",
      "props": [
        {
          "name": "placeholder",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "label",
          "dataType": "string",
          "propType": 1,
          "isOptional": true,
          "config": {
            "layerName": "Label"
          }
        },
        {
          "name": "error",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "isSecure",
          "dataType": "boolean",
          "propType": 4,
          "isOptional": true
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["small", "medium", "large"],
          "config": {
            "layerProp": "Size",
            "valueMapping": {
              "small": ["sm"],
              "medium": ["md"],
              "large": ["lg"]
            }
          }
        }
      ],
      "config": {
        "layerName": "TextField"
      }
    }
  ]
}

Example 4: Avatar Component

Component Code:

import SwiftUI
 
struct AvatarView: View {
    let imageURL: String
    var size: AvatarSize = .medium
    var shape: AvatarShape = .circle
    var showBadge: Bool = false
    
    enum AvatarSize {
        case xs, small, medium, large, xl
        
        var dimension: CGFloat {
            switch self {
            case .xs: return 24
            case .small: return 32
            case .medium: return 48
            case .large: return 64
            case .xl: return 96
            }
        }
    }
    
    enum AvatarShape {
        case circle, square
        
        var cornerRadius: CGFloat {
            switch self {
            case .circle: return 999
            case .square: return 8
            }
        }
    }
    
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            AsyncImage(url: URL(string: imageURL)) { image in
                image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
            } placeholder: {
                Color.gray
            }
            .frame(width: size.dimension, height: size.dimension)
            .cornerRadius(shape.cornerRadius)
            
            if showBadge {
                Circle()
                    .fill(Color.green)
                    .frame(width: 12, height: 12)
                    .overlay(
                        Circle()
                            .stroke(Color.white, lineWidth: 2)
                    )
            }
        }
    }
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./YourApp/Components/AvatarView.swift",
      "name": "AvatarView",
      "props": [
        {
          "name": "imageURL",
          "dataType": "string",
          "propType": 1,
          "isOptional": false,
          "attr": "src"
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["xs", "small", "medium", "large", "xl"],
          "config": {
            "layerProp": "Size"
          }
        },
        {
          "name": "shape",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["circle", "square"],
          "config": {
            "layerProp": "Shape"
          }
        },
        {
          "name": "showBadge",
          "dataType": "boolean",
          "propType": 4,
          "isOptional": true
        }
      ],
      "config": {
        "layerName": "Avatar"
      }
    }
  ]
}

Example 5: List Item Component

Component Code:

import SwiftUI
 
struct ListItemView<LeftIcon: View, RightIcon: View>: View {
    let title: String
    var subtitle: String?
    let leftIcon: LeftIcon?
    let rightIcon: RightIcon?
    var action: () -> Void = {}
    
    init(
        title: String,
        subtitle: String? = nil,
        action: @escaping () -> Void = {},
        @ViewBuilder leftIcon: () -> LeftIcon? = { nil },
        @ViewBuilder rightIcon: () -> RightIcon? = { nil }
    ) {
        self.title = title
        self.subtitle = subtitle
        self.action = action
        self.leftIcon = leftIcon()
        self.rightIcon = rightIcon()
    }
    
    var body: some View {
        Button(action: action) {
            HStack(spacing: 12) {
                if let leftIcon = leftIcon {
                    leftIcon
                }
                
                VStack(alignment: .leading, spacing: 4) {
                    Text(title)
                        .font(.system(size: 16, weight: .semibold))
                        .foregroundColor(.primary)
                    
                    if let subtitle = subtitle {
                        Text(subtitle)
                            .font(.system(size: 14))
                            .foregroundColor(.secondary)
                    }
                }
                
                Spacer()
                
                if let rightIcon = rightIcon {
                    rightIcon
                }
            }
            .padding()
            .background(Color.white)
        }
        .buttonStyle(PlainButtonStyle())
    }
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./YourApp/Components/ListItemView.swift",
      "name": "ListItemView",
      "props": [
        {
          "name": "leftIcon",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "LeftIcon"
          }
        },
        {
          "name": "title",
          "dataType": "string",
          "propType": 1,
          "isOptional": false,
          "config": {
            "layerName": "Title"
          }
        },
        {
          "name": "subtitle",
          "dataType": "string",
          "propType": 1,
          "isOptional": true,
          "config": {
            "layerName": "Subtitle"
          }
        },
        {
          "name": "rightIcon",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "RightIcon"
          }
        }
      ],
      "config": {
        "layerName": "ListItem"
      }
    }
  ]
}

Complete locofy.config.json Example

{
  "components": [
    {
      "path": "./YourApp/Components/CustomButton.swift",
      "name": "CustomButton",
      "props": [
        {
          "name": "label",
          "dataType": "string",
          "propType": 1,
          "isOptional": false
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["small", "medium", "large"],
          "config": {
            "layerProp": "Size"
          }
        },
        {
          "name": "variant",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["primary", "secondary"],
          "config": {
            "layerProp": "Variant"
          }
        }
      ],
      "config": {
        "layerName": "Button"
      }
    },
    {
      "path": "./YourApp/Components/CardView.swift",
      "name": "CardView",
      "props": [
        {
          "name": "header",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "Header"
          }
        },
        {
          "name": "content",
          "dataType": "node",
          "propType": 5,
          "isOptional": false,
          "config": {
            "layerName": "Content"
          }
        }
      ],
      "config": {
        "layerName": "Card"
      }
    },
    {
      "path": "./YourApp/Components/CustomTextField.swift",
      "name": "CustomTextField",
      "props": [
        {
          "name": "placeholder",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "label",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["small", "medium", "large"],
          "config": {
            "layerProp": "Size"
          }
        }
      ],
      "config": {
        "layerName": "TextField"
      }
    }
  ],
  "projectId": "abc123xyz",
  "projectName": "My iOS App"
}

Configuration Properties Reference

PropertyTypeDescription
pathstringRelative path to .swift file
namestringSwiftUI View struct name
propsarrayArray of property definitions
configobjectComponent-level configuration
targetTagsarrayReplace native SwiftUI views

SwiftUI-Specific Considerations

1. ViewBuilder Parameters

SwiftUI components that accept ViewBuilder closures map as node-type props:

{
  "name": "content",
  "dataType": "node",
  "propType": 5
}

2. Binding Properties

@Binding properties for two-way data flow are typically not mapped in the config.

3. Enums

Swift enums map to enum type with string expectValues:

{
  "name": "size",
  "dataType": "enum",
  "expectValues": ["small", "medium", "large"]
}

4. Optional Parameters

Use isOptional: true for optional Swift parameters.


Advanced Features

Target Tags

Replace native SwiftUI views:

{
  "components": [
    {
      "path": "./YourApp/Components/Container.swift",
      "name": "Container",
      "targetTags": ["VStack", "HStack"],
      "props": []
    }
  ]
}

Workflow

  1. Create SwiftUI components with proper property definitions
  2. Run UIPro configuration: "Create UIPro config for all components"
  3. UIPro generates locofy.config.json
  4. Review configuration
  5. Open Figma and map components
  6. Generate iOS code

Best Practices

  1. ✅ Use SwiftUI native types (Color, Font, EdgeInsets)
  2. ✅ Define enums for size and variant props
  3. ✅ Use ViewBuilder for flexible content
  4. ✅ Provide default values for optional parameters
  5. ✅ Use @Binding for two-way data flow
  6. ✅ Keep view composition simple and reusable

Next Steps