Skip to content

Commit 4304cde

Browse files
authored
feat(ios): add Figma Code Connect generation for icons and images (#44)
Add support for generating Figma Code Connect Swift files that link exported assets back to their source Figma components. - Add codeConnectSwift URL config option for iOS icons and images - Store nodeId and fileId in ImagePack for Code Connect URLs - Generate .figma.swift files with FigmaConnect protocol structs - Add Stencil template for Code Connect output - Add comprehensive tests for Code Connect generation
1 parent f9c184e commit 4304cde

15 files changed

Lines changed: 582 additions & 24 deletions

.claude/EXFIG.toon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ terminalUI:
258258
callbacks: Callbacks passed to task groups must be @escaping
259259

260260
templates:
261-
ios[10]: UIColor+extension, Color+extension, UIFont+extension, Font+extension, UIImage+extension, Image+extension, Label, LabelStyle, LabelStyle+extension, header
261+
ios[11]: UIColor+extension, Color+extension, UIFont+extension, Font+extension, UIImage+extension, Image+extension, Label, LabelStyle, LabelStyle+extension, CodeConnect.figma.swift, header
262262
android[6]: colors.xml, Colors.kt, typography.xml, Typography.kt, Icons.kt, header
263263
flutter[4]: colors.dart, icons.dart, images.dart, header
264264
web[8]: theme.css, variables.ts, theme.json, Icon.tsx, types.ts, IconIndex.ts, Image.tsx, ImageIndex.ts, header

CONFIG.md

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ ios:
155155
swiftUIImageSwift: "./Source/Image+extension_icons.swift"
156156
# [optional] Absolute or relative path to swift file where to generate extension for UIImage for accessing icons from the code (e.g. UIImage.ic24ArrowRight)
157157
imageSwift: "./Example/Source/UIImage+extension_icons.swift"
158+
# [optional] Absolute or relative path to swift file where to generate Figma Code Connect structs
159+
codeConnectSwift: "./CodeConnect/Icons.figma.swift"
158160
# Asset render mode: "template", "original" or "default". Default value is "template".
159161
renderMode: default
160162
# Configure the suffix for filtering Icons and to denote a asset render mode: "default".
@@ -175,12 +177,14 @@ ios:
175177
# nameStyle: camelCase
176178
# preservesVectorRepresentation: ["*"]
177179
# imageSwift: "./Generated/ActionsIcons.swift"
180+
# codeConnectSwift: "./CodeConnect/Icons/ActionsIcons.figma.swift"
178181
# - figmaFrameName: Navigation # Export icons from "Navigation" frame
179182
# format: svg
180183
# assetsFolder: Navigation
181184
# nameStyle: camelCase
182185
# preservesVectorRepresentation: ["*"]
183186
# imageSwift: "./Generated/NavigationIcons.swift"
187+
# codeConnectSwift: "./CodeConnect/Icons/NavigationIcons.figma.swift"
184188

185189
# [optional] Parameters for exporting images (legacy single-object format)
186190
# Can also be an array of objects — see "Multiple Images Configuration" section below.
@@ -196,6 +200,8 @@ ios:
196200
swiftUIImageSwift: "./Source/Image+extension_illustrations.swift"
197201
# [optional] Absolute or relative path to swift file where to generate extension for UIImage for accessing illustrations from the code (e.g. UIImage.illZeroNoInternet)
198202
imageSwift: "./Example/Source/UIImage+extension_illustrations.swift"
203+
# [optional] Absolute or relative path to swift file where to generate Figma Code Connect structs
204+
# codeConnectSwift: "./CodeConnect/Images/Illustrations.figma.swift"
199205

200206
# [optional] Parameters for exporting typography
201207
typography:
@@ -449,11 +455,13 @@ ios:
449455
assetsFolder: Actions
450456
nameStyle: camelCase
451457
imageSwift: "./Generated/ActionsIcons.swift"
458+
codeConnectSwift: "./CodeConnect/Icons/ActionsIcons.figma.swift"
452459
- figmaFrameName: Navigation
453460
format: svg
454461
assetsFolder: Navigation
455462
nameStyle: camelCase
456463
imageSwift: "./Generated/NavigationIcons.swift"
464+
codeConnectSwift: "./CodeConnect/Icons/NavigationIcons.figma.swift"
457465
```
458466
459467
### Per-Entry Fields
@@ -466,6 +474,7 @@ Each entry in the array supports all the same fields as the legacy format, plus:
466474
| `nameValidateRegexp` | RegExp pattern for icon name validation. Overrides `common.icons.nameValidateRegexp` |
467475
| `nameReplaceRegexp` | RegExp pattern for name replacement. Overrides `common.icons.nameReplaceRegexp` |
468476
| `nameStyle` | Name style (camelCase, snake_case, etc.). Platform-specific default applies if not set |
477+
| `codeConnectSwift` | (iOS only) Path to Figma Code Connect file for linking assets to Figma components |
469478

470479
### Fallback Behavior
471480

@@ -688,26 +697,29 @@ ios:
688697
assetsFolder: Onboarding
689698
nameStyle: camelCase
690699
imageSwift: "./Generated/OnboardingImages.swift"
700+
codeConnectSwift: "./CodeConnect/Images/OnboardingImages.figma.swift"
691701
- figmaFrameName: Promo
692702
assetsFolder: Promo
693703
nameStyle: camelCase
694704
scales: [1, 2, 3]
695705
imageSwift: "./Generated/PromoImages.swift"
706+
codeConnectSwift: "./CodeConnect/Images/PromoImages.figma.swift"
696707
```
697708

698709
### Per-Entry Fields (iOS)
699710

700-
| Field | Description |
701-
| ------------------- | -------------------------------------------------------------------------------- |
702-
| `figmaFrameName` | Figma frame name to export images from. Overrides `common.images.figmaFrameName` |
703-
| `assetsFolder` | Folder name inside Assets.xcassets |
704-
| `nameStyle` | camelCase, snake_case, PascalCase, etc. |
705-
| `scales` | Array of scale factors [1, 2, 3] (optional) |
706-
| `imageSwift` | Path to UIImage extension file (optional) |
707-
| `swiftUIImageSwift` | Path to SwiftUI Image extension file (optional) |
708-
| `sourceFormat` | Source format from Figma API: `png` (default) or `svg` (optional) |
709-
| `outputFormat` | Output format: `png` (default) or `heic` (optional, macOS only for encoding) |
710-
| `heicOptions` | HEIC encoding options (optional, see below) |
711+
| Field | Description |
712+
| ------------------- | --------------------------------------------------------------------------------- |
713+
| `figmaFrameName` | Figma frame name to export images from. Overrides `common.images.figmaFrameName` |
714+
| `assetsFolder` | Folder name inside Assets.xcassets |
715+
| `nameStyle` | camelCase, snake_case, PascalCase, etc. |
716+
| `scales` | Array of scale factors [1, 2, 3] (optional) |
717+
| `imageSwift` | Path to UIImage extension file (optional) |
718+
| `swiftUIImageSwift` | Path to SwiftUI Image extension file (optional) |
719+
| `codeConnectSwift` | Path to Figma Code Connect file for linking assets to Figma components (optional) |
720+
| `sourceFormat` | Source format from Figma API: `png` (default) or `svg` (optional) |
721+
| `outputFormat` | Output format: `png` (default) or `heic` (optional, macOS only for encoding) |
722+
| `heicOptions` | HEIC encoding options (optional, see below) |
711723

712724
#### HEIC Options (iOS only)
713725

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![CI](https://github.com/alexey1312/ExFig/actions/workflows/ci.yml/badge.svg)](https://github.com/alexey1312/ExFig/actions/workflows/ci.yml)
66
[![Release](https://github.com/alexey1312/ExFig/actions/workflows/release.yml/badge.svg)](https://github.com/alexey1312/ExFig/actions/workflows/release.yml)
77
[![Docs](https://github.com/alexey1312/ExFig/actions/workflows/deploy-docc.yml/badge.svg)](https://alexey1312.github.io/ExFig/documentation/exfig)
8-
![Coverage](https://img.shields.io/badge/coverage-45.99%25-yellow)
8+
![Coverage](https://img.shields.io/badge/coverage-46.48%25-yellow)
99
[![License](https://img.shields.io/github/license/alexey1312/ExFig.svg)](LICENSE)
1010

1111
Command-line utility to export colors, typography, icons, and images from Figma to Xcode, Android Studio, Flutter, and
@@ -36,6 +36,7 @@ Flutter, and React/TypeScript.
3636
### Platform Support
3737

3838
- 📱 SwiftUI and UIKit (iOS/macOS)
39+
- 🔗 Figma Code Connect integration (iOS)
3940
- 🤖 Jetpack Compose and XML resources (Android)
4041
- 🦋 Flutter / Dart
4142
- 🌐 React / TypeScript (CSS variables, TSX components)

Sources/ExFig/Input/Params.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ struct Params: Decodable {
212212

213213
let imageSwift: URL?
214214
let swiftUIImageSwift: URL?
215+
/// URL to generate Figma Code Connect Swift file.
216+
let codeConnectSwift: URL?
215217

216218
let renderMode: XcodeRenderMode?
217219
let renderModeDefaultSuffix: String?
@@ -234,6 +236,8 @@ struct Params: Decodable {
234236

235237
let imageSwift: URL?
236238
let swiftUIImageSwift: URL?
239+
/// URL to generate Figma Code Connect Swift file.
240+
let codeConnectSwift: URL?
237241

238242
let renderMode: XcodeRenderMode?
239243
let renderModeDefaultSuffix: String?
@@ -272,6 +276,7 @@ struct Params: Decodable {
272276
nameReplaceRegexp: nil,
273277
imageSwift: icons.imageSwift,
274278
swiftUIImageSwift: icons.swiftUIImageSwift,
279+
codeConnectSwift: icons.codeConnectSwift,
275280
renderMode: icons.renderMode,
276281
renderModeDefaultSuffix: icons.renderModeDefaultSuffix,
277282
renderModeOriginalSuffix: icons.renderModeOriginalSuffix,
@@ -297,6 +302,8 @@ struct Params: Decodable {
297302

298303
let imageSwift: URL?
299304
let swiftUIImageSwift: URL?
305+
/// URL to generate Figma Code Connect Swift file.
306+
let codeConnectSwift: URL?
300307

301308
let renderMode: XcodeRenderMode?
302309
let renderModeDefaultSuffix: String?
@@ -313,6 +320,8 @@ struct Params: Decodable {
313320
let scales: [Double]?
314321
let imageSwift: URL?
315322
let swiftUIImageSwift: URL?
323+
/// URL to generate Figma Code Connect Swift file.
324+
let codeConnectSwift: URL?
316325
/// Source format for fetching from Figma API. Default: png
317326
let sourceFormat: SourceFormat?
318327
/// Output format for asset catalog. Default: png
@@ -351,6 +360,7 @@ struct Params: Decodable {
351360
scales: images.scales,
352361
imageSwift: images.imageSwift,
353362
swiftUIImageSwift: images.swiftUIImageSwift,
363+
codeConnectSwift: images.codeConnectSwift,
354364
sourceFormat: nil,
355365
outputFormat: nil,
356366
heicOptions: nil,

Sources/ExFig/Loaders/ImageLoaderBase.swift

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,15 @@ class ImageLoaderBase: @unchecked Sendable {
285285
isRTL: isRTL
286286
)
287287
}
288-
return ImagePack(name: packName, images: packImages, platform: platform)
288+
// Get the primary nodeId from the first component (all variants share the same logical node)
289+
let primaryNodeId = components.first?.0
290+
return ImagePack(
291+
name: packName,
292+
images: packImages,
293+
platform: platform,
294+
nodeId: primaryNodeId,
295+
fileId: fileId
296+
)
289297
}
290298
return imagePacks
291299
}
@@ -393,7 +401,15 @@ class ImageLoaderBase: @unchecked Sendable {
393401
isRTL: isRTL
394402
)
395403
}
396-
return ImagePack(name: packName, images: packImages, platform: platform)
404+
// Get the primary nodeId from the first component (all variants share the same logical node)
405+
let primaryNodeId = components.first?.0
406+
return ImagePack(
407+
name: packName,
408+
images: packImages,
409+
platform: platform,
410+
nodeId: primaryNodeId,
411+
fileId: fileId
412+
)
397413
}
398414

399415
return VectorImagesWithHashesResult(
@@ -498,7 +514,15 @@ class ImageLoaderBase: @unchecked Sendable {
498514
isRTL: isRTL
499515
)
500516
}
501-
return ImagePack(name: packName, images: packImages, platform: platform)
517+
// Get the primary nodeId from the first component (all variants share the same logical node)
518+
let primaryNodeId = components.first?.0
519+
return ImagePack(
520+
name: packName,
521+
images: packImages,
522+
platform: platform,
523+
nodeId: primaryNodeId,
524+
fileId: fileId
525+
)
502526
}
503527

504528
return VectorImagesWithHashesResult(
@@ -562,7 +586,15 @@ class ImageLoaderBase: @unchecked Sendable {
562586
)
563587
}
564588
}
565-
return ImagePack(name: packName, images: packImages, platform: platform)
589+
// Get the primary nodeId from the first component (all variants share the same logical node)
590+
let primaryNodeId = components.first?.0
591+
return ImagePack(
592+
name: packName,
593+
images: packImages,
594+
platform: platform,
595+
nodeId: primaryNodeId,
596+
fileId: fileId
597+
)
566598
}
567599
return imagePacks
568600
}
@@ -640,7 +672,15 @@ class ImageLoaderBase: @unchecked Sendable {
640672
)
641673
}
642674
}
643-
return ImagePack(name: packName, images: packImages, platform: platform)
675+
// Get the primary nodeId from the first component (all variants share the same logical node)
676+
let primaryNodeId = components.first?.0
677+
return ImagePack(
678+
name: packName,
679+
images: packImages,
680+
platform: platform,
681+
nodeId: primaryNodeId,
682+
fileId: fileId
683+
)
644684
}
645685

646686
return PNGImagesWithHashesResult(

Sources/ExFig/Subcommands/Export/iOSIconsExport.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ extension ExFigCommand.ExportIcons {
155155
preservesVectorRepresentation: entry.preservesVectorRepresentation,
156156
uiKitImageExtensionURL: entry.imageSwift,
157157
swiftUIImageExtensionURL: entry.swiftUIImageSwift,
158+
codeConnectSwiftURL: entry.codeConnectSwift,
158159
templatesPath: ios.templatesPath
159160
)
160161

Sources/ExFig/Subcommands/Export/iOSImagesExport.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ extension ExFigCommand.ExportImages {
183183
addObjcAttribute: ios.addObjcAttribute,
184184
uiKitImageExtensionURL: entry.imageSwift,
185185
swiftUIImageExtensionURL: entry.swiftUIImageSwift,
186+
codeConnectSwiftURL: entry.codeConnectSwift,
186187
templatesPath: ios.templatesPath,
187188
renderMode: entry.renderMode
188189
)
@@ -443,6 +444,7 @@ extension ExFigCommand.ExportImages {
443444
addObjcAttribute: ios.addObjcAttribute,
444445
uiKitImageExtensionURL: entry.imageSwift,
445446
swiftUIImageExtensionURL: entry.swiftUIImageSwift,
447+
codeConnectSwiftURL: entry.codeConnectSwift,
446448
templatesPath: ios.templatesPath,
447449
renderMode: entry.renderMode
448450
)
@@ -881,6 +883,7 @@ extension ExFigCommand.ExportImages {
881883
addObjcAttribute: ios.addObjcAttribute,
882884
uiKitImageExtensionURL: entry.imageSwift,
883885
swiftUIImageExtensionURL: entry.swiftUIImageSwift,
886+
codeConnectSwiftURL: entry.codeConnectSwift,
884887
templatesPath: ios.templatesPath,
885888
renderMode: entry.renderMode
886889
)
@@ -1021,6 +1024,7 @@ extension ExFigCommand.ExportImages {
10211024
addObjcAttribute: ios.addObjcAttribute,
10221025
uiKitImageExtensionURL: entry.imageSwift,
10231026
swiftUIImageExtensionURL: entry.swiftUIImageSwift,
1027+
codeConnectSwiftURL: entry.codeConnectSwift,
10241028
templatesPath: ios.templatesPath,
10251029
renderMode: entry.renderMode
10261030
)

Sources/ExFigCore/Image.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,26 +117,53 @@ public struct ImagePack: Asset, Sendable {
117117
/// The target platform.
118118
public var platform: Platform?
119119

120+
/// The Figma node ID for this asset (e.g., "12016:2218").
121+
/// Used for Code Connect integration to link back to Figma components.
122+
public var nodeId: String?
123+
124+
/// The Figma file ID containing this asset.
125+
/// Used for Code Connect integration to construct Figma URLs.
126+
public var fileId: String?
127+
120128
/// Creates an image pack with multiple variants.
121129
///
122130
/// - Parameters:
123131
/// - name: The asset name.
124132
/// - images: Array of image variants.
125133
/// - platform: Optional target platform.
126-
public init(name: String, images: [Image], platform: Platform? = nil) {
134+
/// - nodeId: Optional Figma node ID for Code Connect.
135+
/// - fileId: Optional Figma file ID for Code Connect.
136+
public init(
137+
name: String,
138+
images: [Image],
139+
platform: Platform? = nil,
140+
nodeId: String? = nil,
141+
fileId: String? = nil
142+
) {
127143
self.name = name
128144
self.images = images
129145
self.platform = platform
146+
self.nodeId = nodeId
147+
self.fileId = fileId
130148
}
131149

132150
/// Creates an image pack from a single image.
133151
///
134152
/// - Parameters:
135153
/// - image: The single image variant.
136154
/// - platform: Optional target platform.
137-
public init(image: Image, platform: Platform? = nil) {
155+
/// - nodeId: Optional Figma node ID for Code Connect.
156+
/// - fileId: Optional Figma file ID for Code Connect.
157+
public init(
158+
image: Image,
159+
platform: Platform? = nil,
160+
nodeId: String? = nil,
161+
fileId: String? = nil
162+
) {
138163
name = image.name
139164
images = [image]
140165
self.platform = platform
166+
self.nodeId = nodeId
167+
self.fileId = fileId
141168
}
142169
}

Sources/XcodeExport/Model/XcodeImagesOutput.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public struct XcodeImagesOutput {
1212

1313
let uiKitImageExtensionURL: URL?
1414
let swiftUIImageExtensionURL: URL?
15+
/// URL of the swift file where to generate Figma Code Connect structs.
16+
let codeConnectSwiftURL: URL?
1517

1618
let renderMode: XcodeRenderMode?
1719

@@ -20,6 +22,7 @@ public struct XcodeImagesOutput {
2022
/// - preservesVectorRepresentation: A list of image names which should preserve vector data
2123
/// - uiKitImageExtensionURL: URL of the swift file where to generate extension for UIImage class
2224
/// - swiftUIImageExtensionURL: URL of the swift file where to generate extension for Image struct
25+
/// - codeConnectSwiftURL: URL of the swift file where to generate Figma Code Connect structs
2326
/// - renderMode: Xcode Asset Catalog render mode
2427
public init(
2528
assetsFolderURL: URL,
@@ -30,6 +33,7 @@ public struct XcodeImagesOutput {
3033
preservesVectorRepresentation: [String]? = nil,
3134
uiKitImageExtensionURL: URL? = nil,
3235
swiftUIImageExtensionURL: URL? = nil,
36+
codeConnectSwiftURL: URL? = nil,
3337
templatesPath: URL? = nil,
3438
renderMode: XcodeRenderMode? = nil
3539
) {
@@ -41,6 +45,7 @@ public struct XcodeImagesOutput {
4145
self.preservesVectorRepresentation = preservesVectorRepresentation
4246
self.uiKitImageExtensionURL = uiKitImageExtensionURL
4347
self.swiftUIImageExtensionURL = swiftUIImageExtensionURL
48+
self.codeConnectSwiftURL = codeConnectSwiftURL
4449
self.templatesPath = templatesPath
4550
self.renderMode = renderMode
4651
}

0 commit comments

Comments
 (0)