stitch-swiftui-components について
Stitch、ローカル HTML、URL のモバイルレイアウトを、iOS 向けのネイティブ SwiftUI ビューに変換するスキルです。ソース確認、デザイントークン抽出、レイアウト対応付け、Xcode のプロジェクト構成を案内します。
主な機能
- 01モバイルレイアウトを SwiftUI に変換
- 02Stitch、HTML、URL に対応
- 03HTML/CSS のパターンをビューに対応付け
- 04デザイントークンの抽出
- 05Xcode アプリ構成の案内
活用シーン
- 01Stitch の画面からネイティブ iOS ビューを作成
- 02モバイル HTML ページを SwiftUI に変換
- 03モバイル Web ページから SwiftUI 画面を作成
MCP 対応
Stitch をソースにする場合、Stitch MCP でツールを検出し、画面メタデータを取得します。ローカル HTML と URL のソースには MCP は不要です。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ gabelul/ stitch-kit --skill stitch-swiftui-componentsインストール後、Claude Code で/stitch-swiftui-componentsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
resources/architecture-checklist.mdcomponent-template.swiftlayout-mapping.mdscripts/fetch-stitch.sh詳細
Stitch → SwiftUI(ネイティブ iOS)
あなたは Swift/SwiftUI エンジニアです。モバイル UI レイアウト(deviceType: MOBILE で生成された Stitch 画面、ローカル HTML ファイル、または URL)を、Xcode でビルドして実行できるネイティブ iOS SwiftUI ビュー(.swift ファイル)に変換します。Apple のヒューマンインターフェイスガイドラインに従い、iOS に自然になじむコードを作成します。
このスキルを使う場面
次の場合にこのスキルを使います。
- 既存のデザインをネイティブ iOS向けに出力したい場合
- 「SwiftUI」「Xcode」「iOS」「ネイティブ iOS アプリ」といった言葉が出てくる場合
- ソースがモバイルレイアウト(幅が狭く縦長で、タップしやすいサイズのターゲットを持つもの。
deviceType: MOBILEの Stitch 画面、またはモバイル向けに見えるローカルファイル/URL)の場合
**注記:**このスキルは SwiftUI を使う iOS 16 以降を対象としています。クロスプラットフォーム(iOS + Android)向けには、代わりに stitch-react-native-components を使ってください。
前提条件
モバイルレイアウトのソースが必要です。構造と見た目の参考としてのみ読み込みます。ここにあるものが製品に含まれることはなく、SwiftUI ビューは一から作成します。次のいずれかを利用できます。
- Stitch 画面 — Stitch MCP へのアクセスと、
deviceType: MOBILEで生成された画面が必要です - モバイルレイアウトのローカル HTML ファイル — Stitch アカウントは不要です
- モバイルレイアウトを表示するURL — Stitch アカウントは不要です
どの方法を使っても、デスクトップレイアウトを SwiftUI に適切に変換するには大幅な再検討が必要です。
また、以下も必要です。
- macOS 上の Xcode 15 以降
- Swift 5.9 以降
Step 1: ソースを解決する
後続の処理で読み込むファイルは temp/source.html の 1 つだけです。ユーザーから渡されたものに応じた方法で HTML をここに取得したら、Step 2 に進んでください。マークアップの取得元にかかわらず、このスキルの残りの手順は同じです。
このスキルが対応するのは、幅が狭く縦長で、タップしやすいサイズのターゲットを持つモバイルレイアウトのみです。ソースをどの方法で取得した場合でも、デスクトップレイアウトを SwiftUI に適切に変換するには大幅な再検討が必要です。HTML の取得元によって、モバイル向けかどうかの確認方法は異なります。
- Stitch 画面 —
deviceType: MOBILEで生成されたことを確認します。スクリーンショットがデスクトップレイアウトを示している場合は、処理を中止し、まずdeviceType: MOBILEで再生成するようユーザーに伝えてください。 - ローカル HTML ファイルまたは URL — マークアップを確認します。
<meta name="viewport">タグ、モバイルファーストのメディアクエリ、ルートコンテナの狭いmax-width、タップしやすいサイズのタップターゲットが目印です。幅の広い複数列グリッド、ホバーのみの操作、viewport メタタグなしなど、明らかにデスクトップレイアウトの場合は処理を中止し、ソースがモバイルレイアウトではないことをユーザーに伝えてください。「deviceType: MOBILEで再生成してください」とは伝えないでください。Stitch 以外では意味のない指示です。
Stitch 画面の場合:
- 名前空間の確認 —
list_toolsを使って Stitch MCP のプレフィックスを見つけます - メタデータの取得 —
[prefix]:get_screenでデザイン JSON を取得します - HTML のダウンロード — GCS URL には信頼性の高いダウンローダーが必要です。
bash scripts/fetch-stitch.sh "[htmlCode.downloadUrl]" "temp/source.html" - 見た目の確認 — 変換前に
screenshot.downloadUrlを確認します。フル解像度で取得するには、その URL に=s0を追加します。URL をそのまま使うと、API が報告するwidth/heightにかかわらず、512px のサムネイルが返されます。上記の手順に従って、モバイルレイアウトであることを確認してください。
ローカル HTML ファイルの場合:
mkdir -p temp && cp "path/to/design.html" temp/source.html
ファイルを開き、上記の手順に従ってモバイルレイアウトであることを確認します。
URL の場合:
bash scripts/fetch-stitch.sh "https://example.com/page" "temp/source.html"
名前にかかわらず、このスクリプトは汎用の堅牢なダウンローダーです。リダイレクトを追跡し、一時的なエラーを再試行し、gzip を処理し、結果が空の場合は明確に失敗します。URL が Stitch を指しているかどうかは問いません。上記の手順に従って、ページがモバイルレイアウトであることを確認してください。
**スクリーンショットの場合:**アップロードする方法はありません。Stitch MCP API には画像アップロードツールがありません。テキストプロンプトから stitch-mcp-generate-screen-from-text を使ってデザインを再作成するか、HTML を手書きして上記のローカルファイルの手順を使ってください。
API キーが必要なのは Stitch を使う場合のみです。ローカルファイルや URL の変換に Google アカウントは必要ありません。
Step 2: Xcode プロジェクトの構成
MyApp/
├── MyApp.swift ← @main entry point
├── ContentView.swift ← Root view (TabView or NavigationStack)
├── Theme/
│ ├── ThemeTokens.swift ← Design token constants
│ └── Color+App.swift ← Color extension with semantic names
├── Views/
│ ├── [ScreenName]View.swift ← One file per screen
│ └── Components/
│ └── [Name]View.swift ← Reusable component views
├── Models/
│ └── MockData.swift ← Static preview data
└── Assets.xcassets/
└── Colors/ ← Color assets for light/dark mode
Step 3: HTML/CSS から SwiftUI レイアウトへのマッピング
ここが変換の中心です。ソース HTML の各要素に、以下のルールを適用してください。
レイアウトコンテナー
| HTML/CSS パターン | → SwiftUI |
|---|---|
display:flex; flex-direction:column | VStack(alignment: .leading, spacing: 16) |
display:flex; flex-direction:row | HStack(alignment: .center, spacing: 12) |
display:flex; justify-content:space-between | HStack { Spacer() } パターン |
position:absolute オーバーレイ | レイヤー化したビューを使う ZStack |
display:grid(2 列) | LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) |
overflow-y: scroll | ScrollView(.vertical, showsIndicators: false) |
| 項目の繰り返しリスト | List または ScrollView + LazyVStack 内の ForEach |
position:fixed のボトムナビゲーション | TabView(推奨)または Spacer() を使った明示的な VStack |
余白の対応関係
SwiftUI はポイントを使用します(非 Retina では 1pt ≈ 1dp、Retina @2x では 2px)。
// Spacing from Tailwind → SwiftUI points
// p-1(4px)→4 p-2(8px)→8 p-3(12px)→12 p-4(16px)→16
// p-6(24px)→24 p-8(32px)→32 p-12(48px)→48 p-16(64px)→64
ジオメトリの対応関係
// Tailwind rounded- → SwiftUI cornerRadius
// rounded-sm → .cornerRadius(4)
// rounded-md → .cornerRadius(8)
// rounded-lg → .cornerRadius(12)
// rounded-xl → .cornerRadius(16)
// rounded-full → .clipShape(Capsule()) or .cornerRadius(9999)
コンテンツ要素
| HTML | → SwiftUI |
|---|---|
<p>、<span>、テキスト | Text("content") |
<h1> | Text("title").font(.largeTitle).fontWeight(.bold) |
<h2> | Text("title").font(.title2).fontWeight(.semibold) |
<h3> | Text("title").font(.headline) |
<p> の本文 | Text("body").font(.body) |
<small>/キャプション | Text("caption").font(.caption).foregroundStyle(.secondary) |
<img> | リモート画像には AsyncImage(url: URL(string: "..."))、アセットには Image("name") |
<button> プライマリ | Button("Label") { action() }.buttonStyle(.borderedProminent) |
<button> セカンダリ | Button("Label") { action() }.buttonStyle(.bordered) |
<button> ゴースト/テキスト | Button("Label") { action() }.buttonStyle(.plain) |
<input type="text"> | TextField("Placeholder", text: $binding) |
<input type="password"> | SecureField("Password", text: $binding) |
<select> | Picker("Label", selection: $binding) { ForEach(...) } |
<toggle>/チェックボックス | Toggle("Label", isOn: $binding) |
| アイコンのみのボタン | Button { action() } label: { Image(systemName: "xmark") } |
ナビゲーションパターン
| パターン | SwiftUI での実装 |
|---|---|
| 下部タブバー | tabItem { Label("Home", systemImage: "house") } を備えた TabView |
| スタックナビゲーション | NavigationStack { ... NavigationLink(destination: ...) } |
| モーダル/シート | .sheet(isPresented: $showModal) { ModalView() } |
| 全画面モーダル | .fullScreenCover(isPresented: $show) { FullView() } |
| 戻るナビゲーション | NavigationStack で自動処理 |
| アクションシート | .confirmationDialog("Title", isPresented: $show) { ... } |
ステップ 4:SwiftUI のデザイントークン
以下の 16 進数値は、HTML に実際に含まれるトークンソースから、次の順に解決してください。
<head>内のインラインtailwind.config(Stitch が出力するもの)—存在する場合はそのまま使用します。- CSS カスタムプロパティ(
:root { --color-primary: ... })—手書きの HTML やテンプレート化された HTML で一般的です。 - リンクされた、またはインラインのスタイルシート —宣言された色、フォントファミリー、角丸、余白を解析します。
- 最終手段 —マークアップ内で最も頻繁に使われている計算済みの値(背景の基調色、テキスト色、アクセント色、見出し/本文のフォント、角丸)からトークンを推定し、推定した内容をユーザーに伝えて、修正できるようにします。
URL ルートでダウンロードされるのは単一の HTML レスポンスのみです。外部リンクされたスタイルシートが取得できるとは限りません。上記のいずれからもトークンを解決できない場合は、パレットを作り出さず、その旨を伝えてください。
Color 拡張(セマンティックトークン)
// Theme/Color+App.swift
import SwiftUI
extension Color {
// Extract these hex values using the token fallback chain above
// Backgrounds
static let appBackground = Color("AppBackground") // Asset catalog
static let appSurface = Color("AppSurface")
// Brand
static let appPrimary = Color("AppPrimary")
static let appPrimaryFg = Color("AppPrimaryForeground")
// Text
static let appText = Color("AppText")
static let appTextMuted = Color("AppTextMuted")
// Borders
static let appBorder = Color("AppBorder")
}
Color アセットカタログ(ライト+ダーク)
Assets.xcassets/Colors/ に名前付き Color Set を作成します。
各色(例:AppPrimary)について:
- すべての外観:
#6366F1(ライトモードの値) - ダークの外観:
#818CF8(ダーク背景用の明るい色合い)
または、プログラムで定義します(アセットカタログは不要です)。
// Theme/ThemeTokens.swift
import SwiftUI
struct ThemeTokens {
let background: Color
let surface: Color
let primary: Color
let primaryFg: Color
let text: Color
let textMuted: Color
let border: Color
static let light = ThemeTokens(
background: Color(hex: "#FFFFFF"),
surface: Color(hex: "#F4F4F5"),
primary: Color(hex: "#6366F1"),
primaryFg: Color(hex: "#FFFFFF"),
text: Color(hex: "#09090B"),
textMuted: Color(hex: "#71717A"),
border: Color(hex: "#E4E4E7")
)
static let dark = ThemeTokens(
background: Color(hex: "#09090B"),
surface: Color(hex: "#18181B"),
primary: Color(hex: "#818CF8"), // Lightened for dark bg
primaryFg: Color(hex: "#09090B"),
text: Color(hex: "#FAFAFA"),
textMuted: Color(hex: "#A1A1AA"),
border: Color(hex: "#27272A")
)
}
// Convenience: Color from hex string
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let r = Double((int & 0xFF0000) >> 16) / 255
let g = Double((int & 0x00FF00) >> 8) / 255
let b = Double(int & 0x0000FF) / 255
self.init(red: r, green: g, blue: b)
}
}
環境ベースのテーマアクセス
// Anywhere in a view — automatic dark mode
@Environment(\.colorScheme) var colorScheme
var theme: ThemeTokens {
colorScheme == .dark ? .dark : .light
}
// Usage
Text("Hello")
.foregroundStyle(theme.text)
.background(theme.surface)
ステップ 5:コンポーネントテンプレート
// Views/Components/StitchComponentView.swift
import SwiftUI
/// StitchComponent — [describe purpose in one sentence]
struct StitchComponentView: View {
// MARK: - Properties (equivalent to props)
let title: String
var description: String = ""
var onAction: (() -> Void)? = nil
// MARK: - Environment
@Environment(\.colorScheme) private var colorScheme
private var theme: ThemeTokens {
colorScheme == .dark ? .dark : .light
}
// MARK: - State
@State private var isPressed = false
// MARK: - Body
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
.foregroundStyle(theme.text)
if !description.isEmpty {
Text(description)
.font(.subheadline)
.foregroundStyle(theme.textMuted)
}
if let action = onAction {
Button("Action", action: action)
.buttonStyle(.borderedProminent)
.tint(theme.primary)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(theme.surface)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(theme.border, lineWidth: 1)
)
// Minimum touch target — 44pt Apple HIG requirement
.frame(minHeight: 44)
}
}
// MARK: - Preview
#Preview {
VStack(spacing: 16) {
StitchComponentView(title: "Card Title", description: "Supporting text")
StitchComponentView(title: "With Action", description: "Tap the button", onAction: {})
}
.padding()
}
ステップ 6:メインアプリのエントリーポイント
// MyApp.swift
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
// ContentView.swift — root with TabView
struct ContentView: View {
var body: some View {
TabView {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
ProfileView()
.tabItem {
Label("Profile", systemImage: "person")
}
}
}
}
ステップ 7:SwiftUI のアクセシビリティ
SwiftUI はこれらの多くを自動的に処理しますが、必ず確認してください。
// Image accessibility
Image("hero-photo")
.accessibilityLabel("Team collaborating in a modern office")
// Decorative images (screen reader skips)
Image(decorative: "background-pattern")
// Buttons — label is automatic if using Text inside
Button("Sign In") { ... } // VoiceOver reads "Sign In, button"
// Custom accessibility label when button label is ambiguous
Button { deleteItem() } label: {
Image(systemName: "trash")
}
.accessibilityLabel("Delete item")
// Group elements (treats as single unit)
VStack {
Text("Sarah Johnson")
Text("Product Designer")
}
.accessibilityElement(children: .combine)
// Dynamic type support — always use semantic fonts
Text("Headline")
.font(.headline) // ✅ Scales with user's text size
// NOT .font(.system(size: 17, weight: .semibold)) // ❌ Fixed size
ステップ 8:SwiftUI アニメーション
SwiftUI には優れたアニメーション機能が組み込まれています。細かなインタラクションに活用してください。
// Button press spring
Button(action: primaryAction) {
Text("Get Started")
.padding(.horizontal, 24)
.padding(.vertical, 14)
.background(theme.primary)
.foregroundStyle(theme.primaryFg)
.clipShape(Capsule())
.scaleEffect(isPressed ? 0.96 : 1.0)
.animation(.spring(response: 0.2, dampingFraction: 0.6), value: isPressed)
}
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in isPressed = true }
.onEnded { _ in isPressed = false }
)
// Card appear transition
VStack { /* card content */ }
.transition(.move(edge: .bottom).combined(with: .opacity))
// Respect reduced motion
@Environment(\.accessibilityReduceMotion) var reduceMotion
var animation: Animation {
reduceMotion ? .none : .spring(response: 0.3, dampingFraction: 0.7)
}
実行手順
- 確認 — ソースがモバイルレイアウトであることを確認します(ステップ 1 を参照)
- Xcode プロジェクトを作成 — File → New → App を選択し、SwiftUI インターフェースと Swift 言語を指定します
- データレイヤー — デザイン内の静的コンテンツから
Models/MockData.swiftを作成します - テーマ — 抽出した 16 進数値を含む
Theme/ThemeTokens.swiftとColor+App.swiftを作成します - コンポーネント — HTML の各セクションを SwiftUI ビューに変換します(ファイルごとに作業します)
- ナビゲーション —
TabView(タブバー)またはNavigationStack(スタック)を接続します - ビルドと実行 — Xcode で Cmd+R を押します。ライトモードとダークモードの両方でテストします(Simulator で ⌃⌘A を押すと外観を切り替えられます)
トラブルシューティング
| 問題 | 対処方法 |
|---|---|
| ビューが画面からはみ出す | .frame(maxWidth: .infinity) と親の ScrollView を追加します |
| テキストが予期せず省略される | .lineLimit(nil) または .fixedSize(horizontal: false, vertical: true) を追加します |
| ダークモードで色が正しく表示されない | Assets.xcassets の Color Set に Dark appearance が設定されていることを確認します |
| 画像が読み込まれない | AsyncImage の場合は URL が有効か確認します。ローカル画像の場合は、ファイルが Assets.xcassets に含まれている必要があります |
| TabView の項目にラベルが表示されない | コンテンツはラップするビューを挟まず、.tabItem { } の直下に置く必要があります |
| シートを閉じられない | @Environment(\.dismiss) var dismiss を追加し、シート内で dismiss() を呼び出します |
| プレビューがクラッシュする | #Preview に有効なモックデータが設定されていることを確認します。フォールバックなしでオプショナルをアンラップしないでください |
参考資料
resources/component-template.swift— SwiftUI ビューのボイラープレートresources/layout-mapping.md— HTML/CSS → SwiftUI の完全なリファレンスresources/architecture-checklist.md— リリース前チェックリストscripts/fetch-stitch.sh— 信頼性の高い GCS HTML ダウンローダー