火曜日, 6月 24, 2025
火曜日, 6月 24, 2025
- Advertisment -
ホームニューステックニュースFramelink Figma MCPを使用したSwiftUIのコード生成を試行錯誤する

Framelink Figma MCPを使用したSwiftUIのコード生成を試行錯誤する


概要

この記事では、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) が設定されている
  • DividerVStack(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の実装を除くと、下記のツールが定義されていることがわかります。

  
  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は表現されておらず、 strokesborderRadius といった最終的に描画されるレイアウトの情報にまで分解されてしまっています😢

また、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.modecolumn の場合は VStack を、 row の場合は HStack を生成するように指示します。
同様に gappaddingsizing 、そして 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の情報は取得できないため、代わりとなる情報、例えば strokesborderRadius など、を併記するようにします。
そして、画像など正しく取得できない情報は のようなプレースホルダーとして生成するように指示します。

## 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上で gapspacingalignment がすべて正しく指定されていることを前提としています。
故に、より正確なコード生成のためには、デザイナーによる精密な設計力が求められることになります。

自由度の高いデザインには弱い

デザインシステムで定義された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サーバーがオープンベータ版で出ましたね。

https://help.figma.com/hc/ja/articles/32132100833559-Dev-Mode-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もオープンベータ版ではあるので、今後のアップデートに期待したいところです。



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -