mcpField
S
stitch-swiftui-components

stitch-swiftui-components

github.com/gabelul开发4744 次安装14 周前更新
代码仓库

关于 stitch-swiftui-components

此技能将 Stitch、 本地 HTML 或 URL 中的移动端布局转为原生 SwiftUI 视图,面向 iOS 开发者。内容涵盖来源检查、设计令牌提取、布局映射和 Xcode 项目结构。

主要功能

  1. 01将移动布局转换为 SwiftUI
  2. 02支持 Stitch、HTML 或 URL 来源
  3. 03将 HTML 与 CSS 模式映射为视图
  4. 04提取设计令牌
  5. 05规划 Xcode 应用结构

适用场景

  1. 01根据 Stitch 屏幕构建原生 iOS 视图
  2. 02将移动端 HTML 页面转换为 SwiftUI
  3. 03依据移动网页创建 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.md
component-template.swift
layout-mapping.md
scripts/
fetch-stitch.sh

详细介绍

Stitch → SwiftUI(原生 iOS)

你是一名 Swift/SwiftUI 工程师。你可以将移动端 UI 布局——使用 deviceType: MOBILE 生成的 Stitch 屏幕、本地 HTML 文件或 URL——转换为原生 iOS SwiftUI 视图,即可在 Xcode 中构建并运行的 .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+

第 1 步:确定源文件

后续所有步骤都将读取同一个文件:temp/source.html。根据用户提供的来源,将 HTML 获取到该文件中,然后继续第 2 步——无论标记来自哪里,本技能的其余步骤都相同。

本技能仅适用于移动端布局——窄屏、纵向排列、适合触控的目标。无论来源是什么,桌面端布局都无法很好地映射到 SwiftUI,除非进行大量重新设计。确认是否为移动端布局的方式取决于 HTML 的来源:

  • Stitch 屏幕——确认它是使用 deviceType: MOBILE 生成的。如果截图显示的是桌面端布局,请停止并告知用户先使用 deviceType: MOBILE 重新生成。
  • 本地 HTML 文件或 URL——检查标记:是否包含 <meta name="viewport"> 标签、移动端优先的媒体查询、根容器是否设置了较窄的 max-width,以及点击目标是否适合触控。如果明显是桌面端布局(宽屏多列网格、仅悬停交互、没有 viewport 元信息),请停止并告知用户该来源不是移动端布局——不要让他们“使用 deviceType: MOBILE 重新生成”,因为这条指示不适用于 Stitch 以外的来源。

来自 Stitch 屏幕:

  1. 发现命名空间——使用 list_tools 查找 Stitch MCP 前缀
  2. 获取元数据——使用 [prefix]:get_screen 获取设计 JSON
  3. 下载 HTML——GCS URL 需要使用可靠的下载器:
    bash scripts/fetch-stitch.sh "[htmlCode.downloadUrl]" "temp/source.html"
    
  4. 视觉审核——转换前检查 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,然后使用上文的本地文件途径。

只有 Stitch 途径需要 API 密钥。转换本地文件或 URL 完全不需要 Google 账号。

第 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

第 3 步:HTML/CSS → SwiftUI 布局映射

这是核心转换步骤。对源 HTML 中的每个元素应用以下规则:

布局容器
HTML/CSS 模式→ SwiftUI
display:flex; flex-direction:columnVStack(alignment: .leading, spacing: 16)
display:flex; flex-direction:rowHStack(alignment: .center, spacing: 12)
display:flex; justify-content:space-betweenHStack { Spacer() } 模式
position:absolute 覆盖层使用分层视图的 ZStack
display:grid(2 列)LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16)
overflow-y: scrollScrollView(.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 实现
底部标签栏TabView,并使用 tabItem { Label("Home", systemImage: "house") }
堆栈导航NavigationStack { ... NavigationLink(destination: ...) }
模态视图 / 工作表.sheet(isPresented: $showModal) { ModalView() }
全屏模态视图.fullScreenCover(isPresented: $show) { FullView() }
返回导航NavigationStack 自动处理
操作表.confirmationDialog("Title", isPresented: $show) { ... }

第 4 步:SwiftUI 中的设计令牌

按以下顺序,从 HTML 实际使用的令牌源中解析下方的十六进制值:

  1. <head> 中的内联 tailwind.config(Stitch 生成的内容)——若存在,直接使用。
  2. CSS 自定义属性(:root { --color-primary: ... })——手写和模板 HTML 中很常见。
  3. 链接的或内联的样式表——解析其中声明的颜色、字体系列、圆角和间距。
  4. 最后手段——根据标记中出现频率最高的计算值推导令牌(主导背景色、文本颜色、强调色、标题/正文字体、边框圆角),并告知用户这是推断结果,以便他们进行修正。

URL 路由只会下载单个 HTML 响应——外部链接的样式表可能不会随之下载。如果上述方法都无法解析某个令牌,请如实说明,不要自行编造配色方案。

颜色扩展(语义令牌)
// 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")
}
颜色资源目录(浅色 + 深色)

在 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. 验证源内容是否为移动端布局(参见步骤 1)
  2. 创建 Xcode 项目 — 文件 → 新建 → App,选择 SwiftUI 界面和 Swift 语言
  3. 数据层 — 根据设计中的静态内容创建 Models/MockData.swift
  4. 主题 — 创建 Theme/ThemeTokens.swift,填入提取的十六进制颜色值,并创建 Color+App.swift
  5. 组件 — 将 HTML 章节逐个文件转换为 SwiftUI 视图
  6. 导航 — 配置 TabView(标签栏)或 NavigationStack(堆栈)
  7. 构建并运行 — 在 Xcode 中按 Cmd+R。在浅色和深色模式下都进行测试(在 Simulator 中按 ⌃⌘A 可切换外观)

故障排除

问题修复方法
视图溢出屏幕添加 .frame(maxWidth: .infinity),并在父视图中添加 ScrollView
文本意外截断添加 .lineLimit(nil) 或 .fixedSize(horizontal: false, vertical: true)
深色模式下颜色显示不正确确保 Assets.xcassets 中的 Color Set 已设置深色外观
图片未加载对于 AsyncImage,检查 URL 是否有效。对于本地图片,文件必须位于 Assets.xcassets 中
TabView 项目不显示标签内容必须直接放在 .tabItem { } 中,不能用其他视图包裹
Sheet 无法关闭添加 @Environment(\.dismiss) var dismiss,并在 sheet 中调用 dismiss()
预览崩溃检查 #Preview 是否有有效的模拟数据——解包可选值时务必提供回退值

参考资料

  • resources/component-template.swift — SwiftUI 视图样板
  • resources/layout-mapping.md — 完整的 HTML/CSS → SwiftUI 参考
  • resources/architecture-checklist.md — 发布前检查清单
  • scripts/fetch-stitch.sh — 可靠的 GCS HTML 下载器

评论

支持 Markdown