swiftship/internal/skills/data/ui/modern-apis/SKILL.md
Modern SwiftUI APIs: Observable, environment, focus state, sensory feedback. Use when implementing UI patterns related to modern APIs.
npx skillsauth add abdullah4ai/apple-developer-toolkit modern-apisInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This reference covers modern SwiftUI API usage patterns and deprecated API replacements. Always use the latest APIs to ensure forward compatibility and access to new features.
Always use foregroundStyle() instead of foregroundColor().
// Modern (Correct)
Text("Hello")
.foregroundStyle(.primary)
Image(systemName: "star")
.foregroundStyle(.blue)
// Legacy (Avoid)
Text("Hello")
.foregroundColor(.primary)
Why: foregroundStyle() supports hierarchical styles, gradients, and materials, making it more flexible and future-proof.
Always use clipShape(.rect(cornerRadius:)) instead of cornerRadius().
// Modern (Correct)
Image("photo")
.clipShape(.rect(cornerRadius: 12))
VStack {
// content
}
.clipShape(.rect(cornerRadius: 16))
// Legacy (Avoid)
Image("photo")
.cornerRadius(12)
Why: cornerRadius() is deprecated. clipShape() is more explicit and supports all shape types.
Don't apply fontWeight() unless there's a good reason. Always use bold() for bold text.
// Correct
Text("Important")
.bold()
// Avoid (unless you need a specific weight)
Text("Important")
.fontWeight(.bold)
// Acceptable (specific weight needed)
Text("Semibold")
.fontWeight(.semibold)
Always use NavigationStack instead of NavigationView.
// Modern (Correct)
NavigationStack {
List(items) { item in
NavigationLink(value: item) {
Text(item.name)
}
}
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
}
}
// Legacy (Avoid)
NavigationView {
List(items) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item.name)
}
}
}
Use navigationDestination(for:) for type-safe navigation.
struct ContentView: View {
var body: some View {
NavigationStack {
List {
NavigationLink("Profile", value: Route.profile)
NavigationLink("Settings", value: Route.settings)
}
.navigationDestination(for: Route.self) { route in
switch route {
case .profile:
ProfileView()
case .settings:
SettingsView()
}
}
}
}
}
enum Route: Hashable {
case profile
case settings
}
For iOS 18 and later, prefer the Tab API over tabItem() to access modern tab features, and use availability checks or tabItem() for earlier OS versions.
// Modern (Correct) - iOS 18+
TabView {
Tab("Home", systemImage: "house") {
HomeView()
}
Tab("Search", systemImage: "magnifyingglass") {
SearchView()
}
Tab("Profile", systemImage: "person") {
ProfileView()
}
}
// Legacy (Avoid)
TabView {
HomeView()
.tabItem {
Label("Home", systemImage: "house")
}
}
Important: When using Tab(role:) with roles, you must use the new Tab { } label: { } syntax for all tabs. Mixing with .tabItem() causes compilation errors.
// Correct - all tabs use Tab syntax
TabView {
Tab(role: .search) {
SearchView()
} label: {
Label("Search", systemImage: "magnifyingglass")
}
Tab {
HomeView()
} label: {
Label("Home", systemImage: "house")
}
}
// Wrong - mixing Tab and tabItem causes errors
TabView {
Tab(role: .search) {
SearchView()
} label: {
Label("Search", systemImage: "magnifyingglass")
}
HomeView() // Error: can't mix with Tab(role:)
.tabItem {
Label("Home", systemImage: "house")
}
}
Never use onTapGesture() unless you specifically need tap location or tap count. Always use Button otherwise.
// Correct - standard tap action
Button("Tap me") {
performAction()
}
// Correct - need tap location
Text("Tap anywhere")
.onTapGesture { location in
handleTap(at: location)
}
// Correct - need tap count
Image("photo")
.onTapGesture(count: 2) {
handleDoubleTap()
}
// Wrong - use Button instead
Text("Tap me")
.onTapGesture {
performAction()
}
Why: Button provides proper accessibility, visual feedback, and semantic meaning. Use onTapGesture() only when you need its specific features.
Always specify text alongside images in buttons for accessibility.
// Correct - includes text label
Button("Add Item", systemImage: "plus") {
addItem()
}
// Also correct - custom label
Button {
addItem()
} label: {
Label("Add Item", systemImage: "plus")
}
// Wrong - image only, no text
Button {
addItem()
} label: {
Image(systemName: "plus")
}
Never use UIScreen.main.bounds to read available space.
// Wrong - uses UIKit, doesn't respect safe areas
let screenWidth = UIScreen.main.bounds.width
// Correct - use GeometryReader
GeometryReader { geometry in
Text("Width: \(geometry.size.width)")
}
// Better - use containerRelativeFrame (iOS 17+)
Text("Full width")
.containerRelativeFrame(.horizontal)
// Best - let SwiftUI handle sizing
Text("Auto-sized")
.frame(maxWidth: .infinity)
iOS 17+:
containerRelativeFrameandvisualEffectrequire iOS 17 or later.
Don't use GeometryReader if a newer alternative works.
// Modern - containerRelativeFrame
Image("hero")
.resizable()
.containerRelativeFrame(.horizontal) { length, axis in
length * 0.8
}
// Modern - visualEffect for position-based effects
Text("Parallax")
.visualEffect { content, geometry in
content.offset(y: geometry.frame(in: .global).minY * 0.5)
}
// Legacy - only use if necessary
GeometryReader { geometry in
Image("hero")
.frame(width: geometry.size.width * 0.8)
}
Avoid AnyView unless absolutely required.
// Prefer - use @ViewBuilder
@ViewBuilder
func content() -> some View {
if condition {
Text("Option A")
} else {
Image(systemName: "photo")
}
}
// Avoid - type erasure has performance cost
func content() -> AnyView {
if condition {
return AnyView(Text("Option A"))
} else {
return AnyView(Image(systemName: "photo"))
}
}
// Acceptable - when protocol conformance requires it
var body: some View {
// Complex conditional logic that requires type erasure
}
Don't force specific font sizes. Prefer Dynamic Type.
// Correct - respects user's text size preferences
Text("Title")
.font(.title)
Text("Body")
.font(.body)
// Avoid - fixed size doesn't scale
Text("Title")
.font(.system(size: 24))
Avoid using UIKit colors in SwiftUI code.
// Correct - SwiftUI colors
Text("Hello")
.foregroundStyle(.blue)
.background(.gray.opacity(0.2))
// Wrong - UIKit colors
Text("Hello")
.foregroundColor(Color(UIColor.systemBlue))
.background(Color(UIColor.systemGray))
Prefer static member lookup to struct instances.
// Correct - static member lookup
Circle()
.fill(.blue)
Button("Action") { }
.buttonStyle(.borderedProminent)
// Verbose - unnecessary struct instantiation
Circle()
.fill(Color.blue)
Button("Action") { }
.buttonStyle(BorderedProminentButtonStyle())
foregroundStyle() instead of foregroundColor()clipShape(.rect(cornerRadius:)) instead of cornerRadius()Tab API instead of tabItem()Button instead of onTapGesture() (unless need location/count)NavigationStack instead of NavigationViewnavigationDestination(for:) for type-safe navigationAnyView unless requiredUIScreen.main.boundsGeometryReader when alternatives exist.blue vs Color.blue)bold() instead of fontWeight(.bold)tools
Apple platform skill for docs, WWDC lookup, App Store Connect work, and SwiftUI app generation. Use repo-local `node cli.js` for Apple docs and WWDC search, `appledev store` for App Store Connect workflows, and `appledev build` for app scaffolding or fix loops on macOS. USE WHEN: Apple APIs, WWDC sessions, TestFlight/App Store tasks, or building/fixing Apple-platform apps. DON'T USE WHEN: non-Apple platforms, generic backend work, or general web research. EDGE CASES: docs-only queries use `node cli.js` in this repo, not `appledev`; release workflows use `appledev store`; app scaffolding uses `appledev build`; rules-only requests can read `references/ios-rules/` or `references/swiftui-guides/` progressively without invoking binaries.
tools
All-in-one Apple developer skill with three integrated tools shipped as a single unified binary. (1) Documentation search across Apple frameworks, symbols, and 1,267 WWDC sessions from 2014-2025. No credentials needed. (2) App Store Connect CLI with 120+ commands covering builds (find/wait/upload), TestFlight, pre-submission validate, submissions, signing, subscriptions (family-sharable), IAP, analytics, Xcode Cloud, metadata workflows, release pipeline dashboard, insights, win-back offers, promoted purchases, product pages, nominations, accessibility declarations, pre-orders, pricing filters, localizations update, diff, webhooks with local receiver, workflow automation, and more. Requires App Store Connect API key. (3) Multi-platform app builder (iOS/watchOS/tvOS/iPad/macOS/visionOS) that generates complete Swift/SwiftUI apps from natural language with auto-fix, simulator launch, interactive chat mode, and open-in-Xcode. Requires an LLM API key and Xcode. Includes 38 iOS development rules and 12 SwiftUI best practice guides for Liquid Glass, navigation, state management, and modern APIs. All three tools ship as one binary (appledev). USE WHEN: Apple API docs, App Store Connect management, WWDC lookup, or building iOS/watchOS/tvOS/macOS/visionOS apps from scratch. DON'T USE WHEN: non-Apple platforms or general coding.
testing
watchOS complications: WidgetKit complication families, accessory sizes, timeline providers for watch face. Use when implementing watchOS-specific patterns related to widgets.
development
watchOS haptic feedback: WKInterfaceDevice preset haptic types for wrist-based feedback. Use when implementing watchOS-specific patterns related to haptics.