ステップ 1: Figma プロジェクトの準備と設定
Rork Max プラグインのインストール
Figma 内で Rork Max プラグインをインストールします:
- Figma の Community タブから 「Rork Max Code Generation」 を検索
- 「Install」 をクリック
- 初回実行時に API キー(
YOUR_RORK_API_KEY)を設定
- Rork Max ダッシュボードと Figma を連携
デザイントークンの事前整理
Rork Max の変換精度は、Figma での事前整理に大きく依存します。以下の「デザインシステム」を先に構築しておくことが重要です:
カラートークン の定義
Figma → Design System → Colors グループ
Primary
├── primary/50 #F0F7FF
├── primary/100 #E1EFFB
├── primary/200 #C3DDF7
├── primary/400 #86BCF2
├── primary/600 #2563EB
├── primary/700 #1D4ED8
├── primary/900 #1E3A8A
Secondary
├── secondary/600 #F59E0B
├── secondary/700 #D97706
Gray (Neutral)
├── gray/50 #FAFAFA
├── gray/100 #F3F4F6
├── gray/500 #6B7280
├── gray/900 #111827
Rork Max はこれらを自動検出し、生成コード内に Color トークンとして組み込みます:
// 自動生成される ColorPalette
struct ColorPalette {
struct Primary {
static let c50 = Color(red: 0.941, green: 0.969, blue: 1.0)
static let c100 = Color(red: 0.882, green: 0.937, blue: 0.984)
static let c600 = Color(red: 0.145, green: 0.396, blue: 0.933)
static let c700 = Color(red: 0.114, green: 0.306, blue: 0.847)
static let c900 = Color(red: 0.118, green: 0.227, blue: 0.541)
}
struct Secondary {
static let c600 = Color(red: 0.961, green: 0.619, blue: 0.043)
static let c700 = Color(red: 0.855, green: 0.466, blue: 0.024)
}
struct Gray {
static let c50 = Color(red: 0.980, green: 0.980, blue: 0.980)
static let c500 = Color(red: 0.420, green: 0.447, blue: 0.498)
static let c900 = Color(red: 0.067, green: 0.067, blue: 0.067)
}
}
タイポグラフィトークン の定義
Figma → Design System → Typography グループ
Display
├── display/large
│ ├── Font: "Inter" (or SF Pro)
│ ├── Size: 32px
│ ├── Weight: bold (700)
│ └── Line Height: 140%
Heading
├── heading/1
│ ├── Font: "Inter"
│ ├── Size: 28px
│ ├── Weight: bold (700)
│ └── Line Height: 135%
Body
├── body/base
│ ├── Font: "Inter"
│ ├── Size: 16px
│ ├── Weight: regular (400)
│ └── Line Height: 150%
Caption
├── caption/small
│ ├── Font: "Inter"
│ ├── Size: 12px
│ ├── Weight: medium (500)
│ └── Line Height: 140%
自動生成の結果:
struct Typography {
struct Display {
static let large = Font.system(
size: 32,
weight: .bold,
design: .default
)
// または
static let large = Font.custom(
"Inter-Bold",
size: 32
)
}
struct Heading {
static let h1 = Font.system(
size: 28,
weight: .bold,
design: .default
)
}
struct Body {
static let base = Font.system(
size: 16,
weight: .regular,
design: .default
)
}
struct Caption {
static let small = Font.system(
size: 12,
weight: .medium,
design: .default
)
}
}
スペーシングスケール の定義
Figma → Design System → Spacing グループ
Rork Max が認識する標準スケール:
├── spacing/2 = 2px
├── spacing/4 = 4px
├── spacing/8 = 8px
├── spacing/12 = 12px
├── spacing/16 = 16px
├── spacing/24 = 24px
├── spacing/32 = 32px
├── spacing/48 = 48px
└── spacing/64 = 64px
struct Spacing {
static let xs = 4.0
static let sm = 8.0
static let md = 16.0
static let lg = 24.0
static let xl = 32.0
static let xxl = 48.0
}
ステップ 2: Figma コンポーネントの構造化
コンポーネント命名規則(Rork Max 互換形式)
Figma のコンポーネント階層が Rork Max の認識精度を大きく左右します。以下の命名規則に従うことが必須です:
Component Naming Convention:
ComponentType/Variant/State
Examples:
├── Button/Primary/Default
├── Button/Primary/Hover
├── Button/Primary/Active
├── Button/Secondary/Default
├── Button/Tertiary/Disabled
├── TextField/Default/Empty
├── TextField/Default/Focused
├── TextField/Default/Error
├── Card/Base/Default
├── Card/Elevated/Default
├── Badge/Success
├── Badge/Warning
├── Badge/Error
└── Icon/24px/Search
バリアントの効果的な設定
Figma の Variants 機能を最大限活用することで、Rork Max の自動生成精度が飛躍的に向上します:
Button コンポーネント:
┌─ Variants
│ ├─ Style: Primary | Secondary | Tertiary
│ ├─ Size: Small | Medium | Large
│ ├─ State: Default | Hover | Active | Disabled
│ └─ Icon: Yes | No
Rork Max が自動生成する対応 SwiftUI コード:
struct Button: View {
enum Style {
case primary
case secondary
case tertiary
}
enum Size {
case small
case medium
case large
var fontSize: CGFloat {
switch self {
case .small: return 12
case .medium: return 14
case .large: return 16
}
}
var padding: EdgeInsets {
switch self {
case .small: return EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12)
case .medium: return EdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16)
case .large: return EdgeInsets(top: 14, leading: 20, bottom: 14, trailing: 20)
}
}
}
let title: String
let style: Style
let size: Size
let isDisabled: Bool
let icon: String?
let action: () -> Void
@State private var isPressed = false
var body: some View {
Button(action: action) {
HStack(spacing: 8) {
if let icon = icon {
Image(systemName: icon)
.font(.system(size: size.fontSize))
}
Text(title)
.font(.system(size: size.fontSize, weight: .semibold))
}
.padding(size.padding)
.foregroundColor(foregroundColor)
.background(backgroundColor)
.cornerRadius(8)
.opacity(isPressed || isDisabled ? 0.8 : 1.0)
}
.disabled(isDisabled)
.onLongPressGesture(minimumDuration: 0.1) { _ in } onPressingChanged: { isPressing in
isPressed = isPressing
}
}
private var backgroundColor: Color {
if isDisabled {
return ColorPalette.Gray.c100
}
switch style {
case .primary:
return ColorPalette.Primary.c600
case .secondary:
return ColorPalette.Secondary.c600
case .tertiary:
return Color.clear
}
}
private var foregroundColor: Color {
if isDisabled {
return ColorPalette.Gray.c400
}
switch style {
case .primary, .secondary:
return .white
case .tertiary:
return ColorPalette.Primary.c600
}
}
}
Auto Layout の最適な設定
Figma の Auto Layout 設定は、Rork Max の Layout 生成に直接影響します:
Text Label + Input Field(良い例):
┌─ Frame (Auto Layout)
│ ├─ Direction: Horizontal
│ ├─ Spacing: 12
│ ├─ Padding: 16
│ ├─ Alignment: Center
│ └─ Distribution: Space between
│
├─ Label (Text)
│ └─ Size: Fixed width 120px, Auto height
│
└─ Input Field (Rect + Text)
└─ Size: Fill Container, Fixed height 44px
生成される SwiftUI レイアウト:
struct LabeledInput: View {
@State private var text: String = ""
var body: some View {
HStack(spacing: 12) {
Text("Label")
.frame(width: 120, alignment: .leading)
TextField("Placeholder", text: $text)
.frame(height: 44)
.padding(.horizontal, 12)
.background(ColorPalette.Gray.c50)
.cornerRadius(8)
}
.padding(16)
}
}
ステップ 3: Rork Max での自動コード生成と検証
Figma からのエクスポート手順
Figma デザインが準備できたら、Rork Max プラグインを使ってエクスポートします:
- Figma で対象ページ/フレームを選択
- 右パネル → Rork Max プラグイン → 「Export to Rork」 をクリック
- Target Platform を選択(iOS/SwiftUI, Android/React Native, Web/React)
- Design Tokens を確認(自動検出した色・フォント・スペーシングが一覧表示)
- 「Generate Code」 をクリック
生成過程:
Figma Selection
↓
Component Tree 解析
↓
Design Token 自動検出
↓
Layout 制約 変換
↓
SwiftUI/React Native コード生成
↓
Preview 表示 + ダウンロード可能
生成コードの品質検証チェックリスト
Rork Max が生成したコードについて、以下の点を検証してください:
1. デザイントークン の正確さ
// チェック: ColorPalette の値が Figma と完全一致しているか
ColorPalette.Primary.c600 == #2563EB ✓
ColorPalette.Gray.c50 == #FAFAFA ✓
2. レイアウト の完全性
// チェック: HStack/VStack/Spacer が Figma の Auto Layout と一致
// - Spacing 値が正確
// - Padding が正確
// - Alignment が指定通り
3. コンポーネント の構造化
// チェック: Figma バリアント が SwiftUI enum/条件分岐で表現
Button(style: .primary, size: .medium, state: .default)
4. レスポンシブ対応
// チェック: 画面幅変化時の動作が想定通り
@Environment(\.horizontalSizeClass) var sizeClass
var body: some View {
if sizeClass == .compact {
VStack { ... } // 縦積み(モバイル)
} else {
HStack { ... } // 横並び(タブレット・デスクトップ)
}
}
ステップ 4: 実践ワークフロー - E コマースアプリの例
完全なケーススタディ:デザイン → コード生成 → 本番運用
シーン1: Figma でのデザイン構成
E-Commerce App Design System (Figma)
├── Colors
│ ├── Brand Primary: #2563EB
│ ├── Brand Secondary: #F59E0B
│ └── Grayscale: #FAFAFA ~ #111827
│
├── Typography
│ ├── Display: 32px Bold
│ ├── Heading: 24px Bold
│ ├── Body: 16px Regular
│ └── Caption: 12px Regular
│
├── Components
│ ├── Button (Primary/Secondary/Tertiary × Small/Medium/Large)
│ ├── Card (Product/Review/Testimonial)
│ ├── ProductGrid
│ ├── AddToCartFlow
│ ├── Checkout (Address/Payment/Summary)
│ └── NavigationBar
│
└── Screens
├── Home
├── ProductList (with Filters)
├── ProductDetail
├── ShoppingCart
├── Checkout
└── OrderConfirmation
シーン2: Rork Max での自動生成
Figma から HomeScreen フレームをエクスポート → 自動生成コード(抜粋):
// 自動生成: HomeScreen
import SwiftUI
struct HomeScreen: View {
@StateObject private var viewModel = ProductViewModel()
@State private var selectedCategory: String = "all"
var body: some View {
NavigationStack {
VStack(spacing: 0) {
// Header
VStack(alignment: .leading, spacing: 8) {
Text("Welcome Back!")
.font(.system(size: 28, weight: .bold))
.foregroundColor(ColorPalette.Gray.c900)
Text("Find your next favorite product")
.font(.system(size: 14, weight: .regular))
.foregroundColor(ColorPalette.Gray.c600)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(ColorPalette.Primary.c50)
// Category Filter
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(viewModel.categories, id: \.self) { category in
Button(action: {
selectedCategory = category
}) {
Text(category)
.font(.system(size: 14, weight: .medium))
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(
selectedCategory == category ?
ColorPalette.Primary.c600 :
ColorPalette.Gray.c100
)
.foregroundColor(
selectedCategory == category ?
.white :
ColorPalette.Gray.c900
)
.cornerRadius(20)
}
}
}
.padding(.horizontal, 16)
}
.padding(.vertical, 12)
// Product Grid
ScrollView {
LazyVGrid(
columns: [
GridItem(.flexible(), spacing: 16),
GridItem(.flexible(), spacing: 16)
],
spacing: 16
) {
ForEach(viewModel.products, id: \.id) { product in
NavigationLink(destination: ProductDetailScreen(product: product)) {
ProductCard(product: product)
}
}
}
.padding(16)
}
Spacer()
}
.navigationTitle("Shop")
}
.task {
await viewModel.loadProducts(category: selectedCategory)
}
}
}
// 自動生成: ProductCard コンポーネント
struct ProductCard: View {
let product: Product
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Product Image
Image(product.imageName)
.resizable()
.scaledToFill()
.frame(height: 180)
.clipped()
.cornerRadius(12)
// Product Info
VStack(alignment: .leading, spacing: 8) {
Text(product.name)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(ColorPalette.Gray.c900)
.lineLimit(1)
HStack(spacing: 4) {
Image(systemName: "star.fill")
.foregroundColor(.orange)
.font(.system(size: 12))
Text("\(product.rating, specifier: "%.1f")")
.font(.system(size: 14, weight: .medium))
.foregroundColor(ColorPalette.Gray.c700)
}
HStack(spacing: 4) {
Text("¥\(product.price)")
.font(.system(size: 18, weight: .bold))
.foregroundColor(ColorPalette.Primary.c600)
if product.originalPrice > product.price {
Text("¥\(product.originalPrice)")
.font(.system(size: 14, weight: .regular))
.foregroundColor(ColorPalette.Gray.c500)
.strikethrough()
}
}
}
.padding(12)
Spacer()
}
.background(Color.white)
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 4, x: 0, y: 2)
}
}
シーン3: チーム間での変更管理
Figma で設計が変更された場合の運用フロー:
デザイナー: Figma で Button の色を Primary から Secondary に変更
↓
Rork Max プラグイン: 変更を自動検出
↓
デベロッパー: 「Sync from Figma」をクリック
↓
Rork Max: 差分コード(Color トークン + Button コンポーネント)を自動生成
↓
バージョン管理: Git で差分確認 → マージ
ステップ 5: レスポンシブデザインとアニメーション
複数デバイス対応の自動生成
Rork Max は、Figma の Responsive Design 設定を認識し、自動的に SwiftUI の responsive code を生成します:
struct ResponsiveLayout: View {
@Environment(\.horizontalSizeClass) var sizeClass
@Environment(\.verticalSizeClass) var vSize
var body: some View {
Group {
if sizeClass == .compact && vSize == .regular {
// モバイル縦(iPhoneで一般的)
mobilePortraitLayout
} else if sizeClass == .compact && vSize == .compact {
// モバイル横
mobileLandscapeLayout
} else if sizeClass == .regular {
// タブレット / iPad
tabletLayout
}
}
}
var mobilePortraitLayout: some View {
VStack(spacing: 16) {
// スタック型レイアウト
HeaderView()
ContentView()
FooterView()
}
}
var tabletLayout: some View {
HStack(spacing: 24) {
// 左右並列型レイアウト
SidebarView()
.frame(width: 280)
ContentView()
}
}
}
Figma プロトタイプ → Rork コード内 Animation
Figma で指定したトランジション・アニメーションを、Rork Max が自動的に SwiftUI Animation に変換:
struct AnimatedProductCard: View {
@State private var isExpanded = false
var body: some View {
VStack {
// Figma で指定: "Ease-in-out, 300ms"
// ↓ 自動変換
Text("Product Details")
.font(.headline)
.frame(maxWidth: .infinity)
.padding(16)
.background(ColorPalette.Primary.c600)
.foregroundColor(.white)
.onTapGesture {
withAnimation(.easeInOut(duration: 0.3)) {
isExpanded.toggle()
}
}
if isExpanded {
// Figma で指定: "Fade in, 200ms" + "Slide up, 200ms"
VStack(spacing: 12) {
Text("Description")
Text("Price: ¥5,000")
Button("Add to Cart") {}
}
.padding(16)
.transition(.opacity.combined(with: .move(edge: .bottom)))
}
}
.background(Color.white)
.cornerRadius(12)
}
}
ステップ 6: チームワークフローの最適化
デザイナーとデベロッパーの効率的な協業パターン
パターン A: スプリント開始時(新機能開発)
1. デザイナー: Figma で新機能の完全デザイン作成
(全コンポーネント、全バリアント、全スクリーン)
2. デザイナー: Figma デザインシステムを最終確認
- Color トークン存在か
- Typography トークン存在か
- Spacing スケール設定済みか
- コンポーネント命名規則に従っているか
3. デベロッパー: 「Rork Max → Export to Code」実行
4. デベロッパー: 生成コードをレビュー・マージ
(通常 30 分以内で完了)
5. デベロッパー: API 接続・ロジック追加
(生成された View は既に本番品質)
6. QA: デザイン → 実装 の完全一致を確認
パターン B: デザイン変更(既存機能の修正)
デザイナー
↓ Figma で Button 色を変更
Rork Max プラグイン
↓ 変更を検出 → 「Generate Updated Code」
デベロッパー
↓ 差分コードをレビュー(5 行程度の変更に圧縮)
↓ PR → Merge
完了時間: < 5 分
Version Control + Rork Max の統合
# 推奨: デザイン変更時に自動で code 生成 webhook
# GitHub Actions ワークフロー例
name: Auto-sync Figma to Code
on:
workflow_dispatch: # 手動トリガー
jobs:
sync-figma:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Export Figma Design
run: |
curl -X POST https://rork-api.example.com/export \
-H "Authorization: Bearer ${{ secrets.RORK_API_KEY }}" \
-d @figma-config.json
- name: Generate SwiftUI Code
run: npm run rork:generate
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
with:
commit-message: "chore: sync design changes from Figma"
title: "🎨 Design sync from Figma"
body: "Automated Figma → Code generation"
ステップ 7: 本番環境での運用と保守
生成コードのカスタマイズと拡張
Rork Max で生成されたコードは、100% の完成形ではなく、カスタマイズのベースとして設計されています:
// Rork Max 生成コード(読み取り専用ファイル)
// → Generated_Button.swift
struct GeneratedButton: View {
let title: String
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(.system(size: 16, weight: .semibold))
.frame(maxWidth: .infinity)
.padding(12)
.background(ColorPalette.Primary.c600)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
// 開発者による拡張(カスタマイズ可能)
// → Button+Extensions.swift
extension GeneratedButton {
// ローディング状態を追加
@State private var isLoading = false
var bodyWithLoading: some View {
Button(action: {
isLoading = true
action()
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
isLoading = false
}
}) {
HStack {
if isLoading {
ProgressView()
.progressViewStyle(.circular)
} else {
Text(title)
}
}
.frame(maxWidth: .infinity)
.padding(12)
.background(isLoading ? ColorPalette.Gray.c300 : ColorPalette.Primary.c600)
.foregroundColor(.white)
.cornerRadius(8)
}
.disabled(isLoading)
}
}
Design Token アップデート時の一括反映
色やフォントが変更された場合、Figma → Rork で自動生成し直すだけで、全ての UI が一括更新されます:
// 更新前
ColorPalette.Primary.c600 = #2563EB
// Figma で色を変更: #2563EB → #3B82F6
// ↓ Rork Max で再生成
ColorPalette.Primary.c600 = #3B82F6 // 自動更新
// すべての Button・Card・Badge に自動反映 ✓
公式ドキュメントには書かれていない、本番運用で気づいたこと
ここから先は、自分の運営アプリで実際に踏んだ事故と、その後採用した運用ルールを並べます。Figma 公式や Rork のチュートリアルでは触れられない、でも本番のコード生成品質を左右する部分です。
1. デザイントークンの「色番号」命名は、リブランドの初日に破綻する
primary-600 のような色番号ベースの命名は、最初の半年は気持ちよく回ります。しかしブランドカラーをリフレッシュした瞬間に、Figma 上で primary-600 を別の色に書き換えても、Rork が生成済みのコードでは「色を直接ハードコードしていた箇所」が残ります。私の壁紙アプリのリブランド(2024 年)では、132 コンポーネントのうち 19 で #2563EB がそのまま生き残り、レビュー指摘で 3 日吹き飛びました。
❌ 事故った命名(Figma Variables)
primary-600 → #2563EB
primary-700 → #1D4ED8
gray-900 → #111827
✅ 現在の運用(意図ベース命名)
intent.cta.fg → #FFFFFF
intent.cta.bg → #2563EB(後でリブランドしても全 CTA に追従)
intent.cta.bg.hover → #1D4ED8
surface.canvas → #111827
surface.canvas.muted → #1F2937
text.primary → #F9FAFB
text.secondary → #9CA3AF
Rork はこのトークンを SwiftUI / React Native の Color 拡張として書き出してくれるため、Button(intent: .cta) のような呼び出しが自然になります。色番号からの移行は手間ですが、私の場合 4 日かけて 6 アプリ分のトークンを intent.* に揃え直したら、その後のリブランド作業が半日で終わるようになりました。
2. Auto Layout 階層は 4 段以上にしない
Figma の Auto Layout は無限にネストできますが、Rork の生成コードは Auto Layout 階層をそのまま VStack > HStack > VStack > ZStack > VStack のように展開します。階層が深いと SwiftUI なら View 更新範囲が読みにくくなり、React Native なら Hot Reload が遅くなります。
癒し系アプリの「今日のひとこと」画面で、私は当初 7 段までネストしていました。Rork が生成した SwiftUI は 412 行、Hot Reload は 8.2 秒。階層を 3 段に整理したあとは 189 行、1.4 秒に縮みました。
整え方のルール
- ルートは必ず単一の VStack(または ScrollView + VStack)
- その下は「セクション単位」で 1 段だけ HStack/VStack
- セクション内の細かい配置は Spacer と Frame Padding に逃がす
- どうしてもネストしたい場合は、その箇所を Component 化して分離
❌ ネストが深い構造
Frame (root, VStack)
└ Frame (header, HStack)
└ Frame (logo+title, VStack)
└ Frame (badge, HStack)
└ Frame (icon+text, HStack)
└ Frame (text wrapper, VStack)
└ Text
✅ Component を分離してフラットに
Frame (root, VStack)
├ Component: HeaderBar
├ Component: ContentSection
└ Component: FooterCTA
Component 内部のネストは Rork が個別に最適化してくれるため、見かけの「全体ネスト深度」を 3 段に保てます。
3. Variants は 3 軸まで、それ以上はインスタンスの Override で表現する
Button の Variants で color × size × state × icon × loading のような 5 軸を組むと、Figma 上では 27 パターン以上のセルが並びます。Rork はこれを律儀に switch 文として展開するため、生成コードに 60 行近い分岐が現れます。
引き寄せ系アプリの Premium ボタンで、私は最初 5 軸構成にしていました。実装時の switch が読みにくく、新人デベロッパーが「これどの組み合わせのときに走るんですか」と毎週聞いてくる状態に。3 軸(color × size × state)まで削り、icon と loading は Boolean Properties に逃がしたところ、生成コードは 18 行のシンプルな分岐になりました。
実装ルール
- Variants は「見た目が独立して識別される」要素だけ。color / size / state がほぼ全て
- icon の有無、loading 状態、disabled 状態は Boolean Properties で表現する
- text content は当然 Text Properties
4. Boolean Properties は Rork の生成コードと相性がいい
Rork は Boolean Properties を if isLoading { ... } のような分岐に綺麗にコンパイルしますが、Text Properties は文字列なので switch label のようには使えません。条件分岐したい要素は最初から Boolean で持つのが正解です。
❌ Text Properties で分岐したくなる例
Button label = "送信する" | "送信中..." | "送信完了"
これを Rork に渡すと、label の文字列を見て分岐するロジックは生成されません(当然)。
✅ Boolean で表現
Properties:
isLoading: Boolean
isSuccess: Boolean
label: Text (= "送信する")
→ Rork 生成 SwiftUI:
Button {
if isSuccess {
Image(systemName: "checkmark")
} else if isLoading {
ProgressView()
} else {
Text(label)
}
}
私の運用では、isPremium / isLoading / hasIcon の Boolean 3 つで 8 通りの組み合わせを表現できるため、Variants を増やさずに済んでいます。
5. アイコンは Figma に流し込む前に viewBox を 24×24 に統一する
SVG アイコンの viewBox が 0 0 16 16 / 0 0 20 20 / 0 0 24 24 / 0 0 32 32 のように混在していると、Rork は SwiftUI の Image(systemName:) ではなく、SVG をビルド時にラスタライズしてアセットに焼き込みます。Android では PNG 4 解像度分が APK に積まれ、サイズが膨らみます。
引き寄せ系アプリで 312 アイコンを使っていたとき、APK は 8.4 MB ありました。viewBox を 24×24 に統一して SF Symbols 風の SVG に揃えたところ、Rork が SwiftUI の Image(systemName:) 相当の軽量パスを選び、APK は 4.1 MB まで縮みました。
統一スクリプト(Bash + svgo)
#!/usr/bin/env bash
# normalize_icons.sh — Figma 取り込み前の SVG 正規化
set -euo pipefail
INPUT_DIR="$1"
OUTPUT_DIR="${2:-./icons-normalized}"
mkdir -p "$OUTPUT_DIR"
for svg in "$INPUT_DIR"/*.svg; do
name=$(basename "$svg")
# 1. viewBox を 24x24 に統一
npx svgo "$svg" \
--multipass \
--plugins='preset-default,prefixIds' \
-o /tmp/cleaned.svg
# 2. width/height 属性を除去(Figma 側でリサイズさせる)
sed -i -E 's/(width|height)="[^"]*"//g' /tmp/cleaned.svg
# 3. viewBox を強制的に 24x24 に
sed -i -E 's/viewBox="[^"]*"/viewBox="0 0 24 24"/' /tmp/cleaned.svg
cp /tmp/cleaned.svg "$OUTPUT_DIR/$name"
echo "✓ normalized $name"
done
echo "done: $(ls -1 "$OUTPUT_DIR" | wc -l) icons"
このスクリプトを CI に挟むようにしてからは、デザイナーが新しいアイコンを追加するたびにサイズが膨らむ事故は消えました。
6. Auto Layout の Gap は「4 の倍数」に縛る
5px / 7px / 13px のような端数 Gap を許すと、Rork の生成コードに .padding(5) .padding(7) のような独自値が現れ、後で「これ揃ってるんですか」というレビュー指摘が必ず入ります。私の場合は 4 / 8 / 12 / 16 / 24 / 32 / 48 の 7 値だけに縛り、これ以外の Gap を見つけたら Figma の Plugin で警告を出す運用にしました。
警告 Plugin の core(TypeScript)
// figma-plugin/gap-lint.ts
// Figma Plugin で実行: 全フレームの itemSpacing をチェック
const ALLOWED_GAPS = [4, 8, 12, 16, 24, 32, 48];
function lintGaps(node: SceneNode): void {
if (
node.type === "FRAME" &&
node.layoutMode !== "NONE" &&
node.itemSpacing !== figma.mixed
) {
const gap = node.itemSpacing;
if (!ALLOWED_GAPS.includes(gap)) {
// Figma 画面に赤いマーカーを表示
figma.notify(`❌ ${node.name}: gap=${gap}px (許可されていない値)`, {
error: true,
});
// 該当ノードを選択状態に
figma.currentPage.selection = [node];
figma.viewport.scrollAndZoomIntoView([node]);
}
}
if ("children" in node) {
for (const child of node.children) {
lintGaps(child);
}
}
}
figma.currentPage.findAll().forEach(lintGaps);
figma.notify("✓ gap lint complete", { timeout: 1500 });
このルールを導入してから、6 本のアプリのデザインレビュー時間が平均 28 分から 9 分まで縮みました。「揃ってないかも?」という曖昧な議論が消え、Figma 側の lint で機械的に弾けるようになったためです。
次に踏むべき一歩
ここまで読んだあとに、まず一つだけ手を動かすなら、Figma の Variables を intent.* ベースの命名に整え直すことを勧めます。色番号ベースのままだとリブランドのたびに事故が起きますが、命名を変えるだけなら半日で終わり、その後の Rork 生成コードが目に見えて読みやすくなります。
私自身は、6 本のアプリそれぞれで上記 6 つのルールを順番に取り入れてきました。完璧に揃った状態を一度に作ろうとせず、リブランドや新画面追加のタイミングで少しずつ整えていくのが、現実的な進め方だと感じています。
同じ規模で個人運営している方の参考になれば嬉しいです。お読みいただきありがとうございました。