概要
この記事では、Framelink Figma MCPという3rd party製のMCPを使用してFigmaのデザインからSwiftUIのコードをうまく生成するための試行錯誤について記します。
環境
- Editor: Cursor 1.1.4
- Model: Claude 4
まずは素朴に使ってみる
特に工夫せずにFigmaのURLを貼って実装を依頼してみます。
例えば、下記のようなGitHubアプリにおけるプロフィール画面のデザインを今回はお題としてみます。
Copy link to selection
でレイアウトに対応するURLを取得し、Cursorのチャットに貼り付けて下記のように指示を与えます。
@https://www.figma.com/design/...
こちらのデザインに対応するSwiftUIのコードを @ProfileScreen.swift に実装してください。
すると、下記のようなコードが生成されました。
ProfileScreen.swift
import SwiftUI
struct ProfileScreen: View {
var body: some View {
NavigationView {
VStack(spacing: 0) {
ScrollView {
VStack(spacing: 16) {
profileSection
Divider()
.background(Color(red: 0.776, green: 0.776, blue: 0.784))
menuList
Divider()
.background(Color(red: 0.776, green: 0.776, blue: 0.784))
}
.padding(.horizontal, 16)
}
.background(Color(red: 0.949, green: 0.949, blue: 0.969))
Spacer()
}
.background(Color(red: 0.949, green: 0.949, blue: 0.969))
.navigationBarTitleDisplayMode(.inline)
.navigationBarHidden(true)
}
}
private var profileSection: some View {
VStack(spacing: 16) {
VStack(spacing: 16) {
HStack(spacing: 8) {
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 64, height: 64)
.overlay(
Image(systemName: "person.fill")
.foregroundColor(.gray)
.font(.system(size: 24))
)
Text("rockname")
.font(.system(size: 17, weight: .regular))
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
}
Button(action: {}) {
HStack(spacing: 8) {
Image(systemName: "pencil")
.font(.system(size: 16))
.foregroundColor(.black)
Text("Set your status")
.font(.system(size: 17, weight: .regular))
.foregroundColor(.black)
}
.frame(maxWidth: .infinity)
.padding(12)
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color(red: 0.776, green: 0.776, blue: 0.784), lineWidth: 1)
)
.cornerRadius(12)
}
VStack(spacing: 8) {
HStack(spacing: 8) {
Image(systemName: "at")
.font(.system(size: 16))
.foregroundColor(.black)
Text("@_rockname")
.font(.system(size: 15, weight: .medium))
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
}
HStack(spacing: 8) {
Image(systemName: "person.2")
.font(.system(size: 16))
.foregroundColor(.black)
HStack(spacing: 4) {
Text("19")
.font(.system(size: 15, weight: .medium))
.foregroundColor(.black)
Text("followers")
.font(.system(size: 15, weight: .regular))
.foregroundColor(Color(red: 0.235, green: 0.235, blue: 0.263).opacity(0.6))
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.padding(12)
.background(Color(red: 0.984, green: 0.984, blue: 0.992))
.cornerRadius(12)
}
}
private var menuList: some View {
VStack(spacing: 0) {
menuItem(
icon: "folder.fill",
iconColor: .black,
iconBackground: Color.black,
title: "Repositories"
)
Divider()
.background(Color(red: 0.776, green: 0.776, blue: 0.784))
.padding(.leading, 60)
menuItem(
icon: "star.fill",
iconColor: .white,
iconBackground: Color.yellow,
title: "Starred"
)
}
.background(Color.white)
.cornerRadius(12)
}
private func menuItem(icon: String, iconColor: Color, iconBackground: Color, title: String) -> some View {
HStack(spacing: 12) {
HStack {
Image(systemName: icon)
.font(.system(size: 16))
.foregroundColor(iconColor)
.frame(width: 32, height: 32)
.background(iconBackground)
.cornerRadius(8)
}
.frame(width: 32, height: 32)
Text(title)
.font(.system(size: 17, weight: .regular))
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color(red: 0.235, green: 0.235, blue: 0.263).opacity(0.3))
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.white)
}
}
#Preview {
ProfileScreen()
}
Previewはこんな感じです。
全体的にはそれっぽいのですが、コードを覗くと残念ポイントがたくさん見つかります。
残念ポイント
ところどころレイアウトが雰囲気で組まれている
例えば、アバター画像が表示される部分のサイズはFigma上では48×48で指定しています。
しかし、生成されるコードでは64×64で指定されてしまっています。
Circle()
.fill(Color.gray.opacity(0.3))
.frame(width: 64, height: 64)
また、下記のような部分からも、レイアウトのところどころが雰囲気で組まれていることがわかります。
- Figma上では指定していないのに、なぜか
.cornerRadius(12)
が設定されている -
Divider
がVStack(spacing: 16)
の内側に配置されてしまっており、不要なspaceが生まれている -
padding
を付与する箇所が間違っている
画像が雰囲気で設定されている
例えば、XのロゴがFigma上でこのように指定されているのですが…
生成されるコードは Image(systemName: "at")
のようになっており、SF Symbolの at
が使用されてしまっています。
デザインシステムへ準拠していない
例えば、こちらのButtonはデザインシステムで定義されているComponentです。
このComponentに対応する実装は下記のように定義済みでした。
public struct OutlinedButtonStyle: ButtonStyle {
public func makeBody(configuration: Configuration) -> some View {
...
}
}
public extension ButtonStyle where Self == OutlinedButtonStyle {
static var outlined: Self {
.init()
}
}
生成されるコードにおいてもこちらのComponentに対応する実装を使用して欲しいものですが、実際は下記のように1から実装されてしまっています。
Button(action: {}) {
HStack(spacing: 8) {
...
}
.frame(maxWidth: .infinity)
.padding(12)
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color(red: 0.776, green: 0.776, blue: 0.784), lineWidth: 1)
)
.cornerRadius(12)
}
また、各テキストなどにはTypographyやColor等のStyle/VariableがFigma上では適用されています。
もちろん、これらに対応する実装も定義しているので生成されるコードではそちらを参照して欲しいのですが、実際はfont sizeやcolor codeが直接指定されてしまっています。
Text("followers")
.font(.system(size: 15, weight: .regular))
.foregroundColor(Color(red: 0.235, green: 0.235, blue: 0.263).opacity(0.6))
そもそもFramelink Figma MCPで取得できる情報ってどんな感じ?
この辺りの残念ポイントを解消していくために、まずはFramelink Figma MCP経由で取得できるデザインデータの内容を正確に把握していきたいと思います。
Framelink Figma MCPの実装を除くと、下記のツールが定義されていることがわかります。
server.tool(
"get_figma_data",
"When the nodeId cannot be obtained, obtain the layout information about the entire Figma file",
このツールを使って該当ノードからデザインデータを取得しているようですね。
では、具体的にどのような情報が得られるのでしょうか?
Cursor経由で取得データを参照できるので見てみます。
例えば先述の画面から得られるデータは下記の通りです。
超デカいYAML
metadata:
name: xxx
lastModified: '2025-06-21T02:08:14Z'
thumbnailUrl: >-
https://s3-alpha.figma.com/thumbnails/...
nodes:
- id: '123:1185'
name: Profile Screen
type: FRAME
fills: fill_SL5N9G
layout: layout_35O9L2
children:
- id: '123:1190'
name: Frame
type: FRAME
fills: fill_4E6ZRG
layout: layout_EC20WG
children:
- id: '123:1191'
name: Frame
type: FRAME
layout: layout_ICMNP2
children:
- id: '123:1192'
name: Frame
type: FRAME
layout: layout_ICMNP2
children:
- id: '123:1193'
name: Frame
type: FRAME
fills: fill_U90Y1T
layout: layout_0E0XSJ
children:
- id: '123:1194'
name: Frame
type: FRAME
layout: layout_OZNWYU
children:
- id: '123:1195'
name: Circle
type: ELLIPSE
fills: fill_FWIRYQ
layout: layout_BPYFOO
- id: '123:1196'
name: Header
type: TEXT
textStyle: style_1MCD6E
fills: fill_HEHWIB
layout: layout_3PDS52
text: rockname
- id: '123:1198'
name: Button
type: INSTANCE
fills: fill_SL5N9G
strokes: stroke_0AR5BO
layout: layout_HDT31U
borderRadius: 12px
children:
- id: I123:1198;71:1674
name: Frame
type: FRAME
layout: layout_6WJG7W
children:
- id: I123:1198;71:1675
name: Icon
type: INSTANCE
fills: fill_SL5N9G
layout: layout_BPYFOO
children:
- id: I123:1198;71:1675;84:2372
name: Vector
type: IMAGE-SVG
fills: fill_3UKB9E
layout: layout_35O9L2
- id: I123:1198;71:1676
name: Label
type: TEXT
textStyle: style_PQHH12
fills: fill_3UKB9E
layout: layout_JVXW8V
text: Set your status
- id: '123:1199'
name: Frame 1
type: FRAME
layout: layout_USUSLI
children:
- id: '123:1200'
name: Frame
type: FRAME
layout: layout_OZNWYU
children:
- id: '123:1201'
name: x-logo
type: INSTANCE
fills: fill_SL5N9G
layout: layout_BPYFOO
children:
- id: I123:1201;84:2543
name: Vector
type: IMAGE-SVG
fills: fill_3UKB9E
layout: layout_35O9L2
- id: '123:1202'
name: Header
type: TEXT
textStyle: style_KGDC2M
fills: fill_3UKB9E
layout: layout_3PDS52
text: '@_rockname'
- id: '123:1204'
name: Frame
type: FRAME
layout: layout_OZNWYU
children:
- id: '123:1205'
name: people
type: INSTANCE
fills: fill_SL5N9G
layout: layout_BPYFOO
children:
- id: I123:1205;84:2546
name: Vector
type: IMAGE-SVG
fills: fill_3UKB9E
layout: layout_35O9L2
- id: '123:1206'
name: Frame 1
type: FRAME
layout: layout_HLH8XS
children:
- id: '123:1207'
name: Header
type: TEXT
textStyle: style_KGDC2M
fills: fill_3UKB9E
layout: layout_JVXW8V
text: '19'
- id: '123:1208'
name: Header
type: TEXT
textStyle: style_KB7KDE
fills: fill_HEHWIB
layout: layout_3PDS52
text: followers
- id: '123:1210'
name: Separator
type: FRAME
layout: layout_AF0YRW
children:
- id: '123:1211'
name: Separator
type: RECTANGLE
fills: fill_LZ5YXG
layout: layout_1S85SF
- id: '123:1212'
name: Frame
type: FRAME
fills: fill_SL5N9G
layout: layout_ICMNP2
children:
- id: '123:1213'
name: List Item
type: INSTANCE
fills: fill_SL5N9G
layout: layout_9L98UB
children:
- id: I123:1213;49:382
name: state-layer
type: FRAME
layout: layout_V64K5T
children:
- id: I123:1213;49:383
name: _ListItem/Leading/Medium
type: INSTANCE
fills: fill_SL5N9G
layout: layout_8TY78J
children:
- id: I123:1213;49:383;45:4846
name: IconWithBackground
type: FRAME
fills: fill_3UKB9E
layout: layout_NV5AS4
borderRadius: 8px
children:
- id: I123:1213;49:383;45:4843
name: Icon
type: INSTANCE
fills: fill_SL5N9G
layout: layout_BPYFOO
children:
- id: I123:1213;49:383;45:4843;45:4519
name: Vector
type: IMAGE-SVG
fills: fill_SL5N9G
layout: layout_35O9L2
- id: I123:1213;49:384
name: _ListItem/Content/SingleLine/Default
type: INSTANCE
layout: layout_PBF0RT
children:
- id: I123:1213;49:384;49:228
name: Title
type: TEXT
textStyle: style_1MCD6E
fills: fill_3UKB9E
layout: layout_3PDS52
text: Repositories
- id: I123:1213;49:385
name: _ListItem/Trailing
type: INSTANCE
layout: layout_UA7WBJ
children:
- id: I123:1213;49:385;49:79
name: Contents - Trailing
type: FRAME
fills: fill_SL5N9G
layout: layout_R5S3U5
children:
- id: I123:1213;49:385;49:88
name: Icon
type: FRAME
layout: layout_US0E4E
children:
- id: I123:1213;49:385;49:89
name: Drill-in
type: TEXT
textStyle: style_W22DK2
fills: fill_Q2RMCB
layout: layout_3PDS52
text:
- id: '123:1214'
name: Separator
type: FRAME
layout: layout_AF0YRW
children:
- id: '123:1215'
name: Separator
type: RECTANGLE
fills: fill_LZ5YXG
layout: layout_1S85SF
- id: '123:1216'
name: List Item
type: INSTANCE
fills: fill_SL5N9G
layout: layout_9L98UB
children:
- id: I123:1216;49:382
name: state-layer
type: FRAME
layout: layout_V64K5T
children:
- id: I123:1216;49:383
name: _ListItem/Leading/Medium
type: INSTANCE
fills: fill_SL5N9G
layout: layout_8TY78J
children:
- id: I123:1216;49:383;45:4846
name: IconWithBackground
type: FRAME
fills: fill_UVTSWX
layout: layout_NV5AS4
borderRadius: 8px
children:
- id: I123:1216;49:383;45:4843
name: Icon
type: INSTANCE
fills: fill_SL5N9G
layout: layout_BPYFOO
children:
- id: I123:1216;49:383;45:4843;45:4521
name: Vector
type: IMAGE-SVG
fills: fill_SL5N9G
layout: layout_35O9L2
- id: I123:1216;49:384
name: _ListItem/Content/SingleLine/Default
type: INSTANCE
layout: layout_PBF0RT
children:
- id: I123:1216;49:384;49:228
name: Title
type: TEXT
textStyle: style_1MCD6E
fills: fill_3UKB9E
layout: layout_3PDS52
text: Starred
- id: I123:1216;49:385
name: _ListItem/Trailing
type: INSTANCE
layout: layout_UA7WBJ
children:
- id: I123:1216;49:385;49:79
name: Contents - Trailing
type: FRAME
fills: fill_SL5N9G
layout: layout_R5S3U5
children:
- id: I123:1216;49:385;49:88
name: Icon
type: FRAME
layout: layout_US0E4E
children:
- id: I123:1216;49:385;49:89
name: Drill-in
type: TEXT
textStyle: style_W22DK2
fills: fill_Q2RMCB
layout: layout_3PDS52
text:
- id: '123:1217'
name: Frame
type: FRAME
layout: layout_AF0YRW
children:
- id: '123:1218'
name: Separator
type: RECTANGLE
fills: fill_LZ5YXG
layout: layout_1S85SF
- id: '123:1219'
name: Tab Bar - iPhone
type: INSTANCE
layout: layout_35O9L2
children:
- id: I123:1219;49:2096
name: Chrome Material
type: INSTANCE
strokes: stroke_HRGK4Q
layout: layout_35O9L2
children:
- id: I123:1219;49:2096;510:79115
name: Chrome
type: INSTANCE
fills: fill_BMISOQ
effects: effect_OR9TZJ
layout: layout_ELUY7H
- id: I123:1219;49:2097
name: Tab Bar Buttons
type: FRAME
layout: layout_OSGH92
children:
- id: I123:1219;49:2098
name: Tab 1
type: FRAME
layout: layout_1S85SF
children:
- id: I123:1219;49:2099
name: Label
type: TEXT
textStyle: style_GVNMHS
fills: fill_KBZSUD
layout: layout_35O9L2
text: Tab Name
- id: I123:1219;49:2100
name: Symbol
type: TEXT
textStyle: style_8WAFR6
fills: fill_KBZSUD
layout: layout_35O9L2
text:
- id: I123:1219;49:2101
name: Tab 2
type: FRAME
layout: layout_1S85SF
children:
- id: I123:1219;49:2102
name: Label
type: TEXT
textStyle: style_GVNMHS
fills: fill_1D9MPR
layout: layout_35O9L2
text: Tab Name
- id: I123:1219;49:2103
name: Symbol
type: TEXT
textStyle: style_8WAFR6
fills: fill_1D9MPR
layout: layout_35O9L2
text:
- id: I123:1219;49:2104
name: Tab 3
type: FRAME
layout: layout_1S85SF
children:
- id: I123:1219;49:2105
name: Label
type: TEXT
textStyle: style_GVNMHS
fills: fill_1D9MPR
layout: layout_35O9L2
text: Tab Name
- id: I123:1219;49:2106
name: Symbol
type: TEXT
textStyle: style_8WAFR6
fills: fill_1D9MPR
layout: layout_35O9L2
text:
- id: I123:1219;49:2107
name: Tab 4
type: FRAME
layout: layout_1S85SF
children:
- id: I123:1219;49:2108
name: Label
type: TEXT
textStyle: style_GVNMHS
fills: fill_1D9MPR
layout: layout_35O9L2
text: Tab Name
- id: I123:1219;49:2109
name: Symbol
type: TEXT
textStyle: style_8WAFR6
fills: fill_1D9MPR
layout: layout_35O9L2
text:
- id: I123:1219;49:2110
name: Tab 5
type: FRAME
layout: layout_1S85SF
children:
- id: I123:1219;49:2111
name: Label
type: TEXT
textStyle: style_GVNMHS
fills: fill_1D9MPR
layout: layout_35O9L2
text: Tab Name
- id: I123:1219;49:2112
name: Symbol
type: TEXT
textStyle: style_8WAFR6
fills: fill_1D9MPR
layout: layout_35O9L2
text:
- id: '123:1220'
name: Navigation Bar - iPhone (Compact Size Class)
type: INSTANCE
fills: fill_U90Y1T
layout: layout_GRYL5A
children:
- id: I123:1220;49:1026
name: Status Bar - iPhone
type: INSTANCE
layout: layout_J0H6V3
children:
- id: I123:1220;49:1026;2969:19841
name: Frame
type: FRAME
layout: layout_9E6R68
children:
- id: I123:1220;49:1026;2969:18119
name: Time
type: FRAME
layout: layout_27GVH3
children:
- id: I123:1220;49:1026;2969:18120
name: Time
type: TEXT
textStyle: style_TK4IMZ
fills: fill_3UKB9E
layout: layout_JVXW8V
text: '9:41'
- id: I123:1220;49:1026;2969:18121
name: Dynamic Island spacer
type: FRAME
layout: layout_7DOJOL
- id: I123:1220;49:1026;2969:18122
name: Levels
type: FRAME
layout: layout_FFL0K1
children:
- id: I123:1220;49:1026;2969:18123
name: Cellular Connection
type: IMAGE-SVG
fills: fill_3UKB9E
layout: layout_BPYFOO
- id: I123:1220;49:1026;2969:18124
name: Wifi
type: IMAGE-SVG
fills: fill_3UKB9E
layout: layout_BPYFOO
- id: I123:1220;49:1026;2969:18125
name: Battery
type: GROUP
fills: fill_SL5N9G
layout: layout_BPYFOO
borderRadius: 0px 0px 0px 0px
children:
- id: I123:1220;49:1026;2969:18126
name: Border
type: RECTANGLE
strokes: stroke_Q6O0DT
layout: layout_35O9L2
opacity: 0.3499999940395355
borderRadius: 4.300000190734863px
- id: I123:1220;49:1026;2969:18127
name: Cap
type: IMAGE-SVG
fills: fill_3UKB9E
layout: layout_35O9L2
opacity: 0.4000000059604645
- id: I123:1220;49:1026;2969:18128
name: Capacity
type: RECTANGLE
fills: fill_3UKB9E
layout: layout_35O9L2
borderRadius: 2.5px
- id: I123:1220;49:1027
name: Contents
type: FRAME
layout: layout_ICMNP2
children:
- id: I123:1220;49:1029
name: Title and Controls
type: FRAME
layout: layout_1S85SF
- id: '123:1221'
name: Home Indicator
type: FRAME
layout: layout_35O9L2
children:
- id: '123:1222'
name: Home Indicator
type: RECTANGLE
fills: fill_3UKB9E
layout: layout_35O9L2
borderRadius: 100px
globalVars:
styles:
fill_SL5N9G:
- '#FFFFFF'
layout_35O9L2:
mode: none
sizing: {}
fill_4E6ZRG:
- '#F2F2F7'
layout_EC20WG:
mode: column
sizing:
horizontal: fixed
vertical: fixed
dimensions:
width: 402
height: 693
layout_ICMNP2:
mode: column
alignSelf: stretch
sizing:
horizontal: fill
vertical: hug
fill_U90Y1T:
- '#FBFBFD'
layout_0E0XSJ:
mode: column
justifyContent: center
alignSelf: stretch
gap: 16px
padding: 12px 16px
sizing:
horizontal: fill
vertical: hug
layout_OZNWYU:
mode: row
justifyContent: center
alignItems: center
alignSelf: stretch
gap: 8px
sizing:
horizontal: fill
vertical: hug
fill_FWIRYQ:
- type: IMAGE
imageRef: 7f12ea1300756f144a0fb5daaf68dbfc01103a46
scaleMode: FILL
layout_BPYFOO:
mode: none
sizing:
horizontal: fixed
vertical: fixed
style_1MCD6E:
fontFamily: SF Pro
fontWeight: 400
fontSize: 17
lineHeight: 1.2941176470588236em
letterSpacing: '-2.5294118067797493%'
textAlignHorizontal: LEFT
textAlignVertical: CENTER
fill_HEHWIB:
- rgba(60, 60, 67, 0.6)
layout_3PDS52:
mode: none
sizing:
horizontal: fill
vertical: hug
stroke_0AR5BO:
colors:
- '#C6C6C8'
strokeWeight: 1px
layout_HDT31U:
mode: row
justifyContent: center
alignItems: center
alignSelf: stretch
gap: 8px
padding: 12px
sizing:
horizontal: fill
vertical: hug
layout_6WJG7W:
mode: row
justifyContent: center
alignItems: center
gap: 8px
sizing:
horizontal: hug
vertical: hug
fill_3UKB9E:
- '#000000'
style_PQHH12:
fontFamily: SF Pro
fontWeight: 400
fontSize: 17
lineHeight: 1.2941176470588236em
letterSpacing: '-2.5294118067797493%'
textAlignHorizontal: LEFT
textAlignVertical: TOP
layout_JVXW8V:
mode: none
sizing:
horizontal: hug
vertical: hug
layout_USUSLI:
mode: column
justifyContent: center
alignSelf: stretch
gap: 8px
sizing:
horizontal: fill
vertical: hug
style_KGDC2M:
fontFamily: SF Pro
fontWeight: 590
fontSize: 15
lineHeight: 1.3333333333333333em
letterSpacing: '-1.5333333611488342%'
textAlignHorizontal: LEFT
textAlignVertical: CENTER
layout_HLH8XS:
mode: row
justifyContent: center
alignItems: center
gap: 4px
sizing:
horizontal: fill
vertical: hug
style_KB7KDE:
fontFamily: SF Pro
fontWeight: 400
fontSize: 15
lineHeight: 1.3333333333333333em
letterSpacing: '-1.5333333611488342%'
textAlignHorizontal: LEFT
textAlignVertical: CENTER
layout_AF0YRW:
mode: row
justifyContent: stretch
alignItems: stretch
alignSelf: stretch
gap: 10px
sizing:
horizontal: fill
vertical: hug
fill_LZ5YXG:
- '#C6C6C8'
layout_1S85SF:
mode: none
sizing:
horizontal: fill
vertical: fixed
layout_9L98UB:
mode: column
justifyContent: center
alignSelf: stretch
sizing:
horizontal: fill
vertical: hug
layout_V64K5T:
mode: row
alignSelf: stretch
gap: 12px
padding: 4px 16px
sizing:
horizontal: fill
vertical: hug
layout_8TY78J:
mode: column
justifyContent: center
gap: 10px
padding: 4px 0px
sizing:
horizontal: hug
vertical: hug
layout_NV5AS4:
mode: column
justifyContent: center
alignItems: center
sizing:
horizontal: fixed
vertical: fixed
dimensions:
width: 32
height: 32
layout_PBF0RT:
mode: column
justifyContent: center
alignSelf: stretch
sizing:
horizontal: fill
vertical: fill
layout_UA7WBJ:
mode: row
justifyContent: flex-end
alignItems: center
alignSelf: stretch
sizing:
horizontal: hug
vertical: fill
layout_R5S3U5:
mode: row
justifyContent: flex-end
alignItems: center
gap: 16px
sizing:
horizontal: fixed
vertical: hug
dimensions:
width: 24
layout_US0E4E:
mode: column
justifyContent: center
alignItems: center
padding: 4px
sizing:
horizontal: fill
vertical: hug
style_W22DK2:
fontFamily: SF Pro
fontWeight: 590
fontSize: 17
lineHeight: 1.2941176470588236em
textAlignHorizontal: CENTER
textAlignVertical: CENTER
fill_Q2RMCB:
- rgba(60, 60, 67, 0.3)
fill_UVTSWX:
- '#FFCC00'
stroke_HRGK4Q:
colors:
- rgba(0, 0, 0, 0.3)
strokeWeight: 0.3330000042915344px 0px 0px
fill_BMISOQ:
- rgba(255, 255, 255, 0.75)
effect_OR9TZJ:
backdropFilter: blur(50px)
layout_ELUY7H:
mode: column
alignItems: center
sizing:
horizontal: fixed
vertical: hug
dimensions:
width: 402
layout_OSGH92:
mode: row
justifyContent: stretch
alignItems: stretch
gap: 83px
sizing:
horizontal: fixed
vertical: hug
dimensions:
width: 402
style_GVNMHS:
fontFamily: SF Pro
fontWeight: 510
fontSize: 10
lineHeight: 1.193359375em
textAlignHorizontal: CENTER
textAlignVertical: TOP
fill_KBZSUD:
- '#007AFF'
style_8WAFR6:
fontFamily: SF Pro
fontWeight: 510
fontSize: 18
lineHeight: 1.193359375em
textAlignHorizontal: CENTER
textAlignVertical: TOP
fill_1D9MPR:
- '#999999'
layout_GRYL5A:
mode: column
sizing:
horizontal: fixed
vertical: hug
dimensions:
width: 402
layout_J0H6V3:
mode: column
padding: 21px 0px 0px
sizing:
horizontal: fixed
vertical: fixed
dimensions:
width: 402
height: 54
layout_9E6R68:
mode: row
justifyContent: space-between
alignItems: center
alignSelf: stretch
gap: 134px
sizing:
horizontal: fill
vertical: hug
layout_27GVH3:
mode: row
justifyContent: center
alignItems: center
gap: 10px
padding: 0px 6px 0px 16px
sizing:
horizontal: fill
vertical: hug
style_TK4IMZ:
fontFamily: SF Pro
fontWeight: 590
fontSize: 17
lineHeight: 1.2941176470588236em
textAlignHorizontal: CENTER
textAlignVertical: TOP
layout_7DOJOL:
mode: row
justifyContent: center
alignItems: center
sizing:
horizontal: fixed
vertical: fixed
dimensions:
width: 124
height: 10
layout_FFL0K1:
mode: row
justifyContent: center
alignItems: center
gap: 7px
padding: 0px 16px 0px 6px
sizing:
horizontal: fill
vertical: hug
stroke_Q6O0DT:
colors:
- '#000000'
strokeWeight: 1px
非常にデカいですね…!
Framelink Figma MCPは、公開されているFigmaのAPIから取得したデータを整形して返してくれています。
このとき、同じスタイルは変数化するなどしてかなりサイズを小さくして返してくれているのですが、それでもデカいですね…
デカすぎて分析しづらいので、いくつかのパターンのシンプルなデザインから取得できる情報を見ていきます。
Auto Layout
例えば、FigmaにてAuto Layoutで下記のようなレイアウトを組んだとします。
Framelink Figma MCP経由では下記のような情報が得られます。
metadata:
name: xxx
lastModified: '2025-06-21T06:04:11Z'
thumbnailUrl: >-
https://s3-alpha.figma.com/thumbnails/...
nodes:
- id: '90:1309'
name: Vertical Frame
type: FRAME
layout: layout_5EDQNP
children:
- id: '90:1308'
name: Text
type: TEXT
textStyle: style_NFPES7
fills: fill_SHJW8C
layout: layout_0XJOCT
text: Text
- id: '90:1313'
name: Text
type: TEXT
textStyle: style_NFPES7
fills: fill_SHJW8C
layout: layout_0XJOCT
text: Text
globalVars:
styles:
layout_5EDQNP:
mode: column
justifyContent: center
alignItems: center
gap: 8px
padding: 4px 8px
sizing:
horizontal: hug
vertical: hug
style_NFPES7:
fontFamily: Inter
fontWeight: 400
fontSize: 17
lineHeight: 1.2941176470588236em
textAlignHorizontal: LEFT
textAlignVertical: CENTER
fill_SHJW8C:
- '#000000'
layout_0XJOCT:
mode: none
sizing:
horizontal: hug
vertical: hug
各ノードにはlayoutのfieldが存在し、Auto Layoutの設定に対応する記述があることがわかります。
Textが垂直方向に並ぶことや、 gap
, padding
の値がしっかりと取得できていますね 👍
他のパターンも見てみましょう。
水平方向にRectangleとTextが右寄せで並ぶパターンです。
Framelink Figma MCP経由では下記のような情報が得られます。
metadata:
name: xxx
lastModified: '2025-06-21T06:12:22Z'
thumbnailUrl: >-
https://s3-alpha.figma.com/thumbnails/...
nodes:
- id: '127:1184'
name: Horizontal Frame
type: FRAME
layout: layout_OIYFDN
children:
- id: '127:1188'
name: Rectangle
type: RECTANGLE
fills: fill_P9U8UL
layout: layout_J4FVQV
- id: '127:1186'
name: Text
type: TEXT
textStyle: style_HOJJFX
fills: fill_P9H18A
layout: layout_C7QQ70
text: Text
globalVars:
styles:
layout_OIYFDN:
mode: row
justifyContent: flex-end
alignItems: center
gap: 8px
padding: 8px
sizing:
horizontal: fixed
vertical: hug
fill_P9U8UL:
- '#D9D9D9'
layout_J4FVQV:
mode: none
sizing:
horizontal: fixed
vertical: fixed
style_HOJJFX:
fontFamily: Inter
fontWeight: 400
fontSize: 17
lineHeight: 1.2941176470588236em
textAlignHorizontal: LEFT
textAlignVertical: CENTER
fill_P9H18A:
- '#000000'
layout_C7QQ70:
mode: none
sizing:
horizontal: hug
vertical: hug
水平に要素が並ぶことや右寄せであるという情報はうまく取得できていそうです👍
一方で、 サイズがFixedで指定されている場合、肝心のサイズの値が返ってきていない ことがわかります😢
Rectangleは20×20で配置していますが、その値はどこにも見当たりません。
Component
例えば下記のようにButtonのComponentから生成したInstanceを配置したとします。
Framelink Figma MCP経由では下記のような情報が得られます。
metadata:
name: xxx
lastModified: '2025-06-21T06:27:08Z'
thumbnailUrl: >-
https://s3-alpha.figma.com/thumbnails/...
nodes:
- id: '127:1566'
name: Button
type: INSTANCE
fills: fill_MMKOO3
strokes: stroke_3MZHHA
layout: layout_7RUD3K
borderRadius: 12px
children:
- id: I127:1566;71:1674
name: Frame
type: FRAME
layout: layout_DIQZNF
children:
- id: I127:1566;71:1675
name: Icon
type: INSTANCE
fills: fill_MMKOO3
layout: layout_W1PDVB
children:
- id: I127:1566;71:1675;45:4521
name: Vector
type: IMAGE-SVG
fills: fill_0XJ9BW
layout: layout_M64LB7
- id: I127:1566;71:1676
name: Label
type: TEXT
textStyle: style_UG0ZCF
fills: fill_0XJ9BW
layout: layout_PHAMEL
text: Title
globalVars:
styles:
fill_MMKOO3:
- '#FFFFFF'
stroke_3MZHHA:
colors:
- '#C6C6C8'
strokeWeight: 1px
layout_7RUD3K:
mode: row
justifyContent: center
alignItems: center
gap: 8px
padding: 12px
sizing:
horizontal: hug
vertical: hug
layout_DIQZNF:
mode: row
justifyContent: center
alignItems: center
gap: 4px
sizing:
horizontal: hug
vertical: hug
layout_W1PDVB:
mode: none
sizing:
horizontal: fixed
vertical: fixed
fill_0XJ9BW:
- '#000000'
layout_M64LB7:
mode: none
sizing: {}
style_UG0ZCF:
fontFamily: SF Pro
fontWeight: 400
fontSize: 17
lineHeight: 1.2941176470588236em
letterSpacing: '-2.5294118067797493%'
textAlignHorizontal: LEFT
textAlignVertical: TOP
layout_PHAMEL:
mode: none
sizing:
horizontal: hug
vertical: hug
type
にINSTANCEと表示され、 name
にComponentの名前が入っていることがわかります👍
しかし、Variantで用意した Style: Outlined
などのpropertyは表現されておらず、 strokes
や borderRadius
といった最終的に描画されるレイアウトの情報にまで分解されてしまっています😢
また、Button Component内には下記のようにIconのComponentを配置しているのですが…
star
が選択されているものの、この情報もFramelink Figma MCP経由で取得されるデータには載ってきません😢
Style/Variable
例えば下記のようにTypographyおよびColorにてStyle/Variableを適用したレイアウトを組んだとします。
Framelink Figma MCP経由では下記のような情報が得られます。
metadata:
name: xxx
lastModified: '2025-06-21T06:37:27Z'
thumbnailUrl: >-
https://s3-alpha.figma.com/thumbnails/...
nodes:
- id: '128:1574'
name: Group
type: GROUP
layout: layout_SN834M
borderRadius: 0px 0px 0px 0px
children:
- id: '128:1573'
name: Text
type: TEXT
textStyle: style_22NGPX
fills: fill_WSFNXB
layout: layout_SN834M
text: Text
globalVars:
styles:
layout_SN834M:
mode: none
sizing: {}
style_22NGPX:
fontFamily: SF Pro
fontWeight: 400
fontSize: 17
lineHeight: 1.2941176470588236em
letterSpacing: '-2.5294118067797493%'
textAlignHorizontal: LEFT
textAlignVertical: TOP
fill_WSFNXB:
- '#000000'
見てわかる通り、Style/Variableに割り当てた命名の情報は抜け落ち、font sizeやcolor codeの情報のみが取得されます😢
改善ポイント
なぜ残念ポイントが生まれるのか、その理由がわかってきたのではないでしょうか。
以上を踏まえて、どうすればこのFramelink Figma MCPをうまく活用してより良いコードを生成させることができるのか、その改善ポイントについて考えてみます。
トークン数の削減
Figmaのリンクを取得する対象のノードを狭めると、 MCP経由で得られる情報量も小さくなり、コンテキストウィンドウの消費を抑える効果が見込まれます。
例えばiOSアプリのデザインであれば、Status barやTab barを除いたレイアウトのみを選択してリンクをコピーしてあげると良さそうです。
曖昧性を排除するルールの設計
Framelink Figma MCP経由で取得できていない情報があるため、LLMがそこを推論で補ってコードを生成しようとしてしまいます。
そうすると、見た目は正しそうだけど実態としてはデザインシステムが参照されていなくて保守性が低い、などといった状態が生まれてしまいます。
「なんか正しそう」というのは実は一番厄介で、それを見極めて修正していく負荷が大きくなってしまいます。
そのような曖昧性を排除するために、CursorのProject Ruleを設計します。
具体的には、下記の3つのセクションを用意します。
ノードの変換ルール
まずは各ノードの type
に対応するSwiftUIの実装を示します。
## Node 変換
`node.type` に対応するSwiftUIのマッピング。
| Figma Type | SwiftUI |
| -- | -- |
| `TEXT` | `Text` |
| `RECTANGLE` | `Rectangle` |
| `ELLIPSE` | `Circle` |
| `IMAGE-*` | `Image` |
| `GROUP` | `ZStack` |
| `FRAME` | `VStack` or `HStack` ( `layout.mode` に依存) |
| `INSTANCE` | 対応Component |
Layout変換ルール
次に、LayoutのSwiftUIへの変換方針を示します。
例えば、layout.mode
が column
の場合は VStack
を、 row
の場合は HStack
を生成するように指示します。
同様に gap
や padding
、 sizing
、そして alignment
の変換方針を示します。
## Layout 変換
| Key | 値 | 条件 | SwiftUI変換指針 |
| -- | -- | -- | -- |
| `layout.mode` | `column` | | `VStack` |
| `layout.mode` | `row` | | `HStack` |
| `layout.position` | `absolute` | `layout.locationRelativeToParent: (x, y)` | `.offset(x:y:)` を付与し、親を `ZStack(alignment: .topLeading)` へ昇格 |
| `layout.gap` | 数値 px | | `VStack` / `HStack` の `spacing` 引数に数値を適用 |
| `layout.padding` | 数値 px | | `.padding(数値)` |
| `layout.padding` | 数値1 px 数値2 px | | `.padding(.vertical, 数値1)` `.padding(.horizontal, 数値2)` |
| `layout.padding` | 数値1 px 数値2 px 数値3 px 数値4 px | | `.padding(.top, 数値1)` `.padding(.trailing, 数値2)` `.padding(.bottom, 数値3)` `.padding(.leading, 数値4)` |
| `layout.sizing.horizontal` | `fill` | | `.frame(maxWidth: .infinity)` |
| `layout.sizing.horizontal` | `hug` | | 特になし |
| `layout.sizing.horizontal` | `fixed` | | `.frame(width: …)` |
| `layout.sizing.vertical` | `fill` | | `.frame(maxHeight: .infinity)` |
| `layout.sizing.vertical` | `hug` | | 特になし |
| `layout.sizing.vertical` | `fixed` | | `.frame(height: …)` |
### Alignment 変換
| Key | 値 | 条件 | SwiftUI変換指針 |
| -- | -- | -- | -- |
| `layout.alignItems` | `center` | | 特になし ( `alignment: .center` を書いてはいけない) |
| `layout.alignItems` | `null` / `flex-start` | `layout.mode: row` | `HStack(alignment: .top)` |
| `layout.alignItems` | `flex-end` | `layout.mode: row` | `HStack(alignment: .bottom)` |
| `layout.alignItems` | `null` / `flex-start` | `layout.mode: column` | `VStack(alignment: .leading)` |
| `layout.alignItems` | `flex-end` | `layout.mode: column` | `VStack(alignment: .trailing)` |
| `layout.justifyContent` | `center` | | 特になし ( `alignment: .center` を書いてはいけない) |
| `layout.justifyContent` | `null` / `flex-start` | `layout.mode: row` | `.frame(alignment: .leading)` |
| `layout.justifyContent` | `flex-end` | `layout.mode: row` | `.frame(alignment: .trailing)` |
| `layout.justifyContent` | `null` / `flex-start` | `layout.mode: column` | `.frame(alignment: .top)` |
| `layout.justifyContent` | `flex-end` | `layout.mode: column` | `.frame(alignment: .bottom)` |
| `textStyle.textAlignVertical` | `TOP` | `layout.sizing.vertical: fill` | `.frame(alignment: .top)` |
| `textStyle.textAlignVertical` | `CENTER` | `layout.sizing.vertical: fill` | 特になし ( `alignment: .center` を書いてはいけない) |
| `textStyle.textAlignVertical` | `BOTTOM` | `layout.sizing.vertical: fill` | `.frame(alignment: .bottom)` |
| `textStyle.textAlignHorizontal` | `LEFT` | `layout.sizing.horizontal: fill` | `.frame(alignment: .leading)` |
| `textStyle.textAlignHorizontal` | `CENTER` | `layout.sizing.horizontal: fill` | 特になし ( `alignment: .center` を書いてはいけない) |
| `textStyle.textAlignHorizontal` | `RIGHT` | `layout.sizing.horizontal: fill` | `.frame(alignment: .trailing)` |
Component変換ルール
そして、 Component
のセクションを用意して、そこで対応するSwiftUIの実装を示すようにします。
各Variantのpropertyの情報は取得できないため、代わりとなる情報、例えば strokes
や borderRadius
など、を併記するようにします。
そして、画像など正しく取得できない情報は のようなプレースホルダーとして生成するように指示します。
## Component 変換
`node.type: INSTANCE` の場合、 `node.name` に対応するSwiftUIの `Component` を使用する。
以下に各 `Component` のSwiftUI変換指針を示す。
### Button
\```swift
// - `node.strokes: stroke_*`
// - `node.borderRadius: *px`
// - `node.children[0].text: title`
Button(
"title",
action: {}
)
.buttonStyle(.outlined)
\```
\```swift
// - `node.strokes: stroke_*`
// - `node.borderRadius: *px`
// - `node.children[0].name: Icon`
Button(action: {}) {
Image(#T##resource: ImageResource##ImageResource#>)
}
.buttonStyle(.outlined)
\```
\```swift
// - `node.strokes: stroke_*`
// - `node.borderRadius: *px`
// - `node.children[0].name: Icon`
// - `node.children[1].text: title`
Button(
"title",
image: #T##ImageResource#>,
action: {}
)
.buttonStyle(.outlined)
\```
\```swift
// - `node.strokes: stroke_*`
// - `node.borderRadius: *px`
// - `node.children[0].text: title`
// - `node.layout.sizing.horizontal: fill`
Button(
"title",
action: {}
)
.buttonStyle(.outlined(maxWidth: .infinity))
\```
### Chip
...
### List
...
### ListItem
...
制約・最適化のルール
最後に、いくつかのSwiftUIならではの書き方の注意点を示します。
## 制約・最適化
### Frame Modifier ルール
**🚫 禁止パターン**
\```swift
// ❌ 同一frame内でmax系と固定系を併用
.frame(maxWidth: .infinity, height: 100)
\```
**✅ 正しいパターン**
\```swift
// ✅ 別々のframeに分ける(固定→可変の順)
.frame(height: 100)
.frame(maxWidth: .infinity)
// ✅ サイズ指定とalignmentは同一frame内
.frame(maxWidth: .infinity, alignment: .leading)
\```
### Alignment デフォルト値
**🚫 明示してはいけない**
- `HStack(alignment: .center)` → `HStack()`
- `VStack(alignment: .center)` → `VStack()`
- `.frame(alignment: .center)` → `.frame()`
### 冗長性の排除
**子要素のサイズが `.frame(maxWidth: .infinity)` or `.frame(maxHeight: .infinity)` ですでに指定されている場合、親側で同様のframe指定を省略**
\```swift
// ❌ 冗長
VStack {
Text("Hello").frame(maxWidth: .infinity)
}.frame(maxWidth: .infinity) // 不要
// ✅ 最適化
VStack {
Text("Hello").frame(maxWidth: .infinity)
}
\```
そして、色やフォント、画像などの正しく取得できない情報についてはあえて無視するように指示を明示します。
### 出力制限
**以下のmodifierは出力しない**
- `.foregroundStyle()` - 色指定
- `.background()` - 背景色
- `.font()` - フォント指定
### 画像リソース
**具体的なリソース名は指定せず、プレースホルダーを使用**
\```swift
Image(#T##resource: ImageResource##ImageResource#>)
\```
結果発表
改善ポイントを取り入れた上で、FigmaのデザインデータからSwiftUIのコードを生成させてみましょう。
Navigation barやTab barなどを除いたレイアウトを選択してリンクをコピーし、下記のプロンプトへ埋め込みます。
Figma URL : @https://www.figma.com/design/...
ファイル : @ProfileScreen.swift
実装先 : `ProfileContent.body`
1. @figma-to-swiftui-implementation.mdc を読んでFigmaのデザインをSwiftUIの実装へ変換する上でのルールを把握する
2. 上記Figma URLからデザインデータをMCP経由で取得する
3. 取得したデザインデータをルールに従ってSwiftUIの実装へ変換し、実装先に追加する
実行すると、下記のようなコードが生成されました。
private struct ProfileContent: View {
var body: some View {
VStack(spacing: 0) {
VStack(spacing: 16) {
HStack(spacing: 8) {
Circle()
.frame(width: 80, height: 80)
Text("rockname")
.frame(maxWidth: .infinity, alignment: .leading)
}
Button(
"Set your status",
image: #T##ImageResource#>,
action: {}
)
.buttonStyle(.outlined(maxWidth: .infinity))
VStack(spacing: 8) {
HStack(spacing: 8) {
Image(#T##resource: ImageResource##ImageResource#>)
.frame(width: 16, height: 16)
Text("@_rockname")
.frame(maxWidth: .infinity, alignment: .leading)
}
HStack(spacing: 8) {
Image(#T##resource: ImageResource##ImageResource#>)
.frame(width: 16, height: 16)
HStack(spacing: 4) {
Text("19")
Text("followers")
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.padding(.vertical, 12)
.padding(.horizontal, 16)
Rectangle()
.frame(height: 1)
.frame(maxWidth: .infinity)
VStack(spacing: 0) {
ListItem(
content: .singleLine(title: "Repositories"),
leading: .medium(.iconWithBackground(icon: #T##Icon#>, background: #T##Color#>)),
trailing: ListItem.Trailing(icon: #T##ListItem.Trailing.Icon#>)
)
Rectangle()
.frame(height: 1)
.frame(maxWidth: .infinity)
ListItem(
content: .singleLine(title: "Starred"),
leading: .medium(.iconWithBackground(icon: #T##Icon#>, background: #T##Color#>)),
trailing: ListItem.Trailing(icon: #T##ListItem.Trailing.Icon#>)
)
}
Rectangle()
.frame(height: 1)
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
}
プレースホルダーとして生成された画像部分に適切なリソースを設定していきます。
@@ -24,7 +24,7 @@ private struct ProfileContent: View {
// Set status button
Button(
"Set your status",
- image: ,
+ image: Icon.smiley.imageResource,
action: {}
)
.buttonStyle(.outlined(maxWidth: .infinity))
@@ -33,7 +33,7 @@ private struct ProfileContent: View {
VStack(spacing: 8) {
// X (Twitter) account
HStack(spacing: 8) {
- Image()
+ Image(Icon.xLogo.imageResource)
.frame(width: 16, height: 16)
Text("@_rockname")
@@ -42,7 +42,7 @@ private struct ProfileContent: View {
// Followers
HStack(spacing: 8) {
- Image()
+ Image(Icon.people.imageResource)
.frame(width: 16, height: 16)
HStack(spacing: 4) {
@@ -65,8 +65,8 @@ private struct ProfileContent: View {
VStack(spacing: 0) {
ListItem(
content: .singleLine(title: "Repositories"),
- leading: .medium(.iconWithBackground(icon: , background: )),
- trailing: ListItem.Trailing(icon: )
+ leading: .medium(.iconWithBackground(icon: .repo, background: .black)),
+ trailing: ListItem.Trailing(icon: .chevronRight)
)
Rectangle()
@@ -75,8 +75,8 @@ private struct ProfileContent: View {
ListItem(
content: .singleLine(title: "Starred"),
- leading: .medium(.iconWithBackground(icon: , background: )),
- trailing: ListItem.Trailing(icon: )
+ leading: .medium(.iconWithBackground(icon: .star, background: .yellow)),
+ trailing: ListItem.Trailing(icon: .chevronRight)
)
}
Previewするとこのようになりました。
いい感じですね。
あとはここへサイズやフォント, 色を設定していきます。
@@ -15,9 +15,11 @@ private struct ProfileContent: View {
// Avatar and name
HStack(spacing: 8) {
Circle()
- .frame(width: 80, height: 80)
+ .frame(width: 48, height: 48)
Text("rockname")
+ .font(.body)
+ .foregroundStyle(Color(.secondaryLabel))
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -34,20 +36,30 @@ private struct ProfileContent: View {
// X (Twitter) account
HStack(spacing: 8) {
Image(Icon.xLogo.imageResource)
+ .foregroundStyle(Color(.label))
.frame(width: 16, height: 16)
Text("@_rockname")
+ .foregroundStyle(Color(.label))
+ .font(.subheadline)
+ .fontWeight(.bold)
.frame(maxWidth: .infinity, alignment: .leading)
}
// Followers
HStack(spacing: 8) {
Image(Icon.people.imageResource)
+ .foregroundStyle(Color(.label))
.frame(width: 16, height: 16)
HStack(spacing: 4) {
Text("19")
+ .foregroundStyle(Color(.label))
+ .font(.subheadline)
+ .fontWeight(.bold)
Text("followers")
+ .foregroundStyle(Color(.secondaryLabel))
+ .font(.subheadline)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -57,9 +69,7 @@ private struct ProfileContent: View {
.padding(.horizontal, 16)
// Separator
- Rectangle()
- .frame(height: 1)
- .frame(maxWidth: .infinity)
+ Divider()
// List section
VStack(spacing: 0) {
@@ -69,10 +79,8 @@ private struct ProfileContent: View {
trailing: ListItem.Trailing(icon: .chevronRight)
)
- Rectangle()
- .frame(height: 1)
- .frame(maxWidth: .infinity)
-
+ Divider()
+
ListItem(
content: .singleLine(title: "Starred"),
leading: .medium(.iconWithBackground(icon: .star, background: .yellow)),
@@ -81,9 +89,7 @@ private struct ProfileContent: View {
}
// Bottom separator
- Rectangle()
- .frame(height: 1)
- .frame(maxWidth: .infinity)
+ Divider()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
はい、おおよそ完成です 🎉
コード生成の誤りを見つけて修正する手間はなく、基本的にはプレースホルダーを埋めてサイズや色, フォントを指定していくのみに集中することができています!
懸念ポイント
当然良いことばかりではないので、いくつか懸念ポイントも書いておきます。
デザイナーへ求められるAuto Layout力
今回、ルールをガチガチに固めることによってSwiftUIのコード生成の曖昧性を排除するというアプローチを取りました。
これは、Figma上で gap
や spacing
、 alignment
がすべて正しく指定されていることを前提としています。
故に、より正確なコード生成のためには、デザイナーによる精密な設計力が求められることになります。
自由度の高いデザインには弱い
デザインシステムで定義されたComponentを使えば使うほどコード生成の曖昧性は排除されますが、その逆もまた然りであり、自由度の高いデザインについてはうまくコード生成ができません。
例えば、今回のケースであまり触れなかったのですが、Z軸上で重ねて表示するデザインについてはFramelink Figma MCP経由で取得できる情報がかなり限られており、うまくSwiftUIのコードで再現することが途端に難しくなります。
保守性の低いルール
Componentの変換ルールを見て「げっ…」と思った方も少なくないでしょう。
Button(
"title",
action: {}
)
.buttonStyle(.outlined)
コメント部分で strokes
の有無や children
の要素について言及しており、これを全Componentの各Variantで表現していくのは骨の折れる作業ですし、何よりComponentの定義が変わった際の更新コストが重く、保守性が低いと言わざるを得ません。
(Framelink Figma MCP経由で各Variantのpropertyの情報を取得できればこの辺りの運用コストをグッと下げられるのですが…)
まとめ
本記事ではFramelink Figma MCPを使用して、FigmaのデザインからSwiftUIのコードを生成する方法について解説しました。
このMCPでは、Auto Layout、Component、Style/Variableに関する情報を取得できますが、以下のような制限があることがわかりました:
- Fixed指定されたサイズの値が取得できない
- Componentのproperty情報を取得できない
- Style/Variableの命名情報が取得できない
これらの制限に対して、以下のアプローチで改善を図りました:
- トークン数削減のため、生成対象のノードを必要最小限に絞る
- 曖昧性を排除するための詳細なルールを設計する
- 画像やスタイル名など取得できない情報はプレースホルダーとして生成する
これにより、デザインシステムに準拠した手戻りの少ないSwiftUIコードを生成することができました。
本記事がDesign to Codeを試行するみなさんにとって少しでも参考になりましたら幸いです。
余談: Figma公式MCPについて
そういえばFigma公式のMCPサーバーがオープンベータ版で出ましたね。
公式のMCPでは下記4つのツールが公開されています:
- get_code: Figmaデスクトップアプリで選択中のノード、または指定したノードIDに対応するReactのUIコードを生成します。
-
get_variable_defs: 指定したノードIDに対応するVariableの定義を取得します。例えば、
{'icon/default/secondary': #949494}
のようなフォント、色、サイズ、余白などのデザインプロパティに適用可能な再利用値を取得できます。 -
get_code_connect_map: コードベース内のコンポーネントとFigmaのノードのマッピング情報を取得します。例えば、
{[nodeId]: {codeConnectSrc: e.g. location of component in codebase, codeConnectName: e.g. name of component in codebase}}. E.g. {'1:2': { codeConnectSrc: 'https://github.com/foo/components/Button.tsx', codeConnectName: 'Button' }}
のような情報を返します。 - get_image: Figmaデスクトップアプリで選択中のノード、または指定したノードIDに対応する画像を生成します。
公式のMCPには、Figmaで表現されている情報を過不足なくよりシンプルに返してもらうことを期待していました。
しかし、Reactのコードへ変換した上で返ってくるというような状況であり、さらにそのコード上においても依然としてどのVariableが適用されているか等の情報は欠落していました。
ただ、Componentの紐付けについては Code Connect をちゃんと使えば get_code_connect_map
経由でうまくいけそうな雰囲気を感じることはできました (ビジネス以上のプランへの契約は必要になるが) 。
しばらくは使うとしてもFramelink Figma MCPを自分は選ぶだろうなと思いつつ、まだ公式のMCPもオープンベータ版ではあるので、今後のアップデートに期待したいところです。
Views: 0