From skip-app-design
Builds cross-platform SwiftUI views for iOS and Android using SkipUI. Covers supported components, navigation, state management, Jetpack Compose customization, and Material 3 theming.
How this skill is triggered — by the user, by Claude, or both
Slash command
/skip-app-design:building-skip-uiThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
SkipUI converts SwiftUI into Jetpack Compose for Android. Use standard SwiftUI patterns — SkipUI handles the translation. Not all SwiftUI APIs are available; this skill covers what works and how to customize the Android rendering.
SkipUI converts SwiftUI into Jetpack Compose for Android. Use standard SwiftUI patterns — SkipUI handles the translation. Not all SwiftUI APIs are available; this skill covers what works and how to customize the Android rendering.
A Skip app uses standard SwiftUI with the SkipUI framework:
// Package.swift dependencies
.package(url: "https://source.skip.tools/skip.git", from: "1.2.0"),
.package(url: "https://source.skip.tools/skip-ui.git", from: "1.0.0"),
.package(url: "https://source.skip.tools/skip-foundation.git", from: "1.0.0"),
Create projects with:
skip init --appid=com.example.myapp my-app MyApp # Lite
skip init --native-app --appid=com.example.myapp my-app MyApp # Fuse
Consult the reference files for detailed tables:
references/components.md — full component support tablereferences/compose-customization.md — Compose and Material 3 customisationreferences/patterns.md — common cross-platform UI patternsreferences/unimplemented-apis.md — the catalog of SwiftUI APIs marked @available(*, unavailable) in SkipUI, organised by file and category. Consult this when a build error says "This API is not yet available in Skip."VStack, HStack, ZStack, Group, Spacer, GeometryReader, ScrollView, LazyVStack, LazyHStack, LazyVGrid, LazyHGrid, Grid, GridRow, Form, Section
Button, Text, Label, TextField, SecureField, TextEditor, Toggle, Slider, Stepper, Picker, ProgressView, Link, Menu, ShareLink
NavigationStack, NavigationLink, NavigationSplitView, TabView, .sheet, .fullScreenCover, .alert, .confirmationDialog, .toolbar, .searchable
Image, AsyncImage, Color, LinearGradient, Circle, Capsule, Rectangle, RoundedRectangle, Path
@State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, @Environment, @Bindable, @Observable (via SkipModel)
DatePicker: Medium support (date range constraints need Fuse bridging)@AppStorage: Optional values not supportedCanvas: Basic drawing onlyUIViewRepresentable: Not available — use #if os(iOS) guardsmatchedGeometryEffect: Not available@FetchRequest: Not available — use SkipSQL or SkipFirebaseShape with complex AnimatableData: Not available.contentShape(Rectangle()): Not implemented yet — emits "This API is not yet available in Skip" at transpile time. Drop the modifier or wrap in #if !SKIP. The Button hit-target it usually expands is large enough on its own when the label is an HStack containing a Spacer().ToolbarItem(placement: .topBarTrailing) { ... }: topBarTrailing is unavailable on macOS. Skip's Package.swift declares macOS(.v14) alongside iOS(.v17), so any Skip target that builds for macOS will fail with "topBarTrailing is unavailable in macOS". Use .primaryAction instead — it has the same visual placement on iOS, is available on macOS, and transpiles to a Compose TopAppBar action slot:
.toolbar {
ToolbarItem(placement: .primaryAction) { // not .topBarTrailing
Menu { ... } label: { Image("more_vert", bundle: .module) }
}
}
(Image(systemName: "ellipsis") would happen to render on Android because ellipsis is one of the ~50 SF Symbol names SkipUI maps internally — but you should still ship a .symbolset. See skip-icons for why.)Array.remove(atOffsets: IndexSet): this is provided by SwiftUI's extension on RangeReplaceableCollection, not by Foundation. A pure-model module that doesn't import SwiftUI will not have it. Either import SwiftUI in the model, or expose a plain func remove(at: [Int]) and let the view convert Array(offsets):
// In TodoAppModel/ViewModel.swift — no SwiftUI import needed
public func remove(at indices: [Int]) {
for index in indices.sorted(by: >) where index < items.count {
items.remove(at: index)
}
}
// In TodoApp/ContentView.swift — SwiftUI is imported, IndexSet → [Int]
.onDelete { offsets in
viewModel.remove(at: Array(offsets))
}
import SwiftUI
struct ContentView: View {
@State private var count = 0
var body: some View {
NavigationStack {
VStack(spacing: 20) {
Text("Count: \(count)")
.font(.title)
Button("Increment") {
count += 1
}
.buttonStyle(.borderedProminent)
}
.navigationTitle("My App")
}
}
}
import Foundation
import Observation
@Observable final class ViewModel {
var items: [String] = []
var isLoading = false
func loadItems() async {
isLoading = true
defer { isLoading = false }
// Fetch data...
}
}
var body: some View {
Text("Hello")
#if os(Android)
.font(.system(size: 18))
#endif
}
Embed raw Jetpack Compose in #if SKIP blocks:
#if SKIP
ComposeView { context in
androidx.compose.material3.FloatingActionButton(
onClick: { handleTap() }
) {
androidx.compose.material3.Icon(
imageVector: androidx.compose.material.icons.Icons.Default.Add,
contentDescription: "Add"
)
}
}
#endif
Apply Compose modifiers to SwiftUI views:
Text("Custom")
#if SKIP
.composeModifier { modifier in modifier.testTag("customText") }
#endif
Customize Material 3 theming:
ContentView()
.material3ColorScheme(ColorScheme(primary: Color.blue, onPrimary: Color.white))
The hard rule for Skip projects: never use Image(systemName:) or Label(_, systemImage:). Always ship a .symbolset resource and call Image("name", bundle: .module).
SF Symbols are an Apple-only catalogue with no Android equivalent. Almost every systemName: call you write will render a blank on Android — the iOS build resolves the name against Apple's catalogue, the Android build has nothing to resolve it against. Even for the ~50 names SkipUI hardcodes a Material Icon substitute for, the rendering isn't pixel-identical to the SF Symbol you asked for, and the hardcoded map can change between SkipUI releases. The reliable answer in all cases is a single .symbolset that ships to both platforms and renders identically.
Download Material Symbols from https://fonts.google.com/icons in Apple symbolset format (not raw SVG), drop into Sources/<Module>/Resources/Icons.xcassets/<name>.symbolset/, and reference with Image("name", bundle: .module):
// ❌ Will render blank on Android.
Label("Bookmark", systemImage: "bookmark.fill")
Image(systemName: "checklist")
// ✅ Works on both platforms; localisation-correct; predictable.
Label {
Text("Bookmark", bundle: .module, comment: "bookmark menu label")
} icon: {
Image("bookmark", bundle: .module)
}
The trailing-closure Label { Text } icon: { Image } form is the canonical shape — it makes the label translatable (the String Catalog extractor sees the Text(_, bundle: .module, comment: …)) and the icon cross-platform (the .symbolset resource ships to both platforms via the same module bundle).
For the full workflow — picking glyphs, downloading the Apple-format SVG, the .symbolset directory layout with Contents.json, app launcher icons via skip icon, and Android-side rendering quirks — see the skip-icons skill.
User-facing strings should be created with the bundle-aware Text constructor so they're extracted into the module's Resources/Localizable.xcstrings String Catalog and translated for both platforms:
Text("Settings", bundle: .module, comment: "settings sheet title")
Label {
Text("Block Ads", bundle: .module, comment: "settings toggle for blocking ads")
} icon: {
Image("shield", bundle: .module)
}
Skip transpiles the catalog to Android string resources at build time, so a single source produces translated UI on both iOS and Android. For the full translation workflow — catalog schema, translation states, xcstringstool validation, byte-budgeted store metadata, and the App Store / Google Play locale mapping — see the skip-localization skill.
Consult these resources for detailed guidance:
npx claudepluginhub skiptools/skills --plugin skip-app-designGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Resolves in-progress git merge or rebase conflicts by analyzing history, understanding intent, and preserving both changes where possible. Runs automated checks after resolution.