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.xcodeprojSwiftUI 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
| Property | Type | Description |
|---|---|---|
path | string | Relative path to .swift file |
name | string | SwiftUI View struct name |
props | array | Array of property definitions |
config | object | Component-level configuration |
targetTags | array | Replace 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
- Create SwiftUI components with proper property definitions
- Run UIPro configuration:
"Create UIPro config for all components" - UIPro generates
locofy.config.json - Review configuration
- Open Figma and map components
- Generate iOS code
Best Practices
- ✅ Use SwiftUI native types (Color, Font, EdgeInsets)
- ✅ Define enums for size and variant props
- ✅ Use ViewBuilder for flexible content
- ✅ Provide default values for optional parameters
- ✅ Use
@Bindingfor two-way data flow - ✅ Keep view composition simple and reusable
Next Steps
- Check the main Custom Components guide
- Explore other guides: React Native, Kotlin
- Join our Discord community (opens in a new tab)