internal/skills/content/swiftui/SKILL.md
SwiftUI framework guardrails, patterns, and best practices for AI-assisted development. Use when working with SwiftUI projects, or when the user mentions SwiftUI. Provides declarative UI patterns, state management, Combine, navigation, and accessibility guidelines.
npx skillsauth add ar4mirez/samuel swiftuiInstall 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.
Applies to: SwiftUI 5.0+ (iOS 17+, macOS 14+, watchOS 10+, tvOS 17+, visionOS 1.0+) Language Guide: @.claude/skills/swift-guide/SKILL.md
SwiftUI is Apple's declarative UI framework for building native apps across all Apple platforms. It uses a reactive, data-driven approach where the UI automatically updates when state changes.
Use SwiftUI when:
Consider alternatives when:
UIViewRepresentable)body into private computed properties at 30+ lines// MARK: - Subviews to group computed subview propertiesbody (use view models or .task)private; use @ViewBuilder for conditional helpers@State: View-local value-type state (booleans, strings, simple structs)@Binding: Two-way connection to parent state@Observable (iOS 17+): Preferred for view models; replaces ObservableObject@StateObject: Owned ObservableObject (iOS 14-16; created once, survives re-renders)@ObservedObject: Non-owned ObservableObject passed from parent@EnvironmentObject / @Environment: Shared state and system values@ObservedObject for state the view owns@StateObject in a computed propertyPascalCase + View suffix (ProfileView, SettingsView)PascalCase without suffix (StatCard, AvatarImage)camelCase (cardStyle(), loading(_:))PascalCase descriptive name (UserViewModel, AuthManager)LazyVStack/LazyHStack for scrollable lists, LazyVGrid/LazyHGrid for grids.task for async data loading (auto-cancels on disappear)AnyView type erasure; use @ViewBuilder or Group insteadAsyncImage with placeholder and error states for remote imagesEquatable conformance with .equatable() for expensive viewsMyApp/
├── MyApp/
│ ├── App/
│ │ └── MyApp.swift # @main entry point
│ ├── Features/
│ │ ├── Authentication/
│ │ │ ├── Views/ # LoginView.swift, SignUpView.swift
│ │ │ ├── ViewModels/ # AuthViewModel.swift
│ │ │ └── Models/ # User.swift
│ │ ├── Home/
│ │ │ ├── Views/ | ViewModels/ | Components/
│ │ └── Settings/
│ ├── Core/
│ │ ├── Network/ # APIClient.swift, Endpoints.swift
│ │ ├── Storage/
│ │ └── Extensions/ # View+Extensions.swift
│ ├── Shared/
│ │ ├── Components/ # LoadingView, ErrorView, PrimaryButton
│ │ └── Modifiers/ # CardModifier.swift
│ ├── Resources/ # Assets.xcassets, Localizable.strings
│ └── Preview Content/
├── MyAppTests/
└── MyAppUITests/
Features/ groups by domain with Views, ViewModels, Models, ComponentsShared/ for cross-feature reusable views and custom modifiersCore/ for non-UI infrastructure (networking, storage, extensions)@main
struct MyApp: App {
@State private var appState = AppState()
@State private var authManager = AuthManager()
var body: some Scene {
WindowGroup {
ContentView()
.environment(appState)
.environment(authManager)
}
}
}
struct ContentView: View {
@Environment(AuthManager.self) private var authManager
var body: some View {
Group {
if authManager.isAuthenticated { MainTabView() }
else { AuthenticationView() }
}
.animation(.easeInOut, value: authManager.isAuthenticated)
}
}
Use @Environment(\.scenePhase) with .onChange(of:) for active/inactive/background transitions.
Decompose views with computed properties; extract reusable pieces as separate structs.
struct UserProfileView: View {
let user: User
@State private var isEditing = false
var body: some View {
VStack(spacing: 16) { profileHeader; statsSection; actionButtons }
.padding()
.navigationTitle("Profile")
.sheet(isPresented: $isEditing) { EditProfileView(user: user) }
}
// MARK: - Subviews
private var profileHeader: some View {
VStack(spacing: 8) {
AsyncImage(url: URL(string: user.avatarURL)) { image in
image.resizable().aspectRatio(contentMode: .fill)
} placeholder: { Circle().fill(Color.gray.opacity(0.3)) }
.frame(width: 100, height: 100).clipShape(Circle())
Text(user.name).font(.title2).fontWeight(.semibold)
Text(user.email).font(.subheadline).foregroundStyle(.secondary)
}
}
private var statsSection: some View {
HStack(spacing: 32) {
StatView(title: "Posts", value: user.postCount)
StatView(title: "Followers", value: user.followerCount)
}.padding(.vertical)
}
private var actionButtons: some View {
HStack(spacing: 16) {
Button("Edit Profile") { isEditing = true }.buttonStyle(.borderedProminent)
Button("Share") { }.buttonStyle(.bordered)
}
}
}
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
}
}
extension View {
func cardStyle() -> some View { modifier(CardModifier()) }
func loading(_ isLoading: Bool) -> some View { modifier(LoadingModifier(isLoading: isLoading)) }
}
@Observable
final class UserViewModel {
var user: User?
var isLoading = false
var errorMessage: String?
private let userService: UserServiceProtocol
init(userService: UserServiceProtocol = UserService()) {
self.userService = userService
}
func loadUser(id: String) async {
isLoading = true; errorMessage = nil
do { user = try await userService.fetchUser(id: id) }
catch { errorMessage = error.localizedDescription }
isLoading = false
}
}
struct UserView: View {
@State private var viewModel = UserViewModel()
let userId: String
var body: some View {
Group {
if viewModel.isLoading { ProgressView() }
else if let user = viewModel.user { UserProfileView(user: user) }
else if let error = viewModel.errorMessage {
ErrorView(message: error) { Task { await viewModel.loadUser(id: userId) } }
}
}
.task { await viewModel.loadUser(id: userId) }
}
}
struct APIClientKey: EnvironmentKey {
static let defaultValue: APIClientProtocol = APIClient()
}
extension EnvironmentValues {
var apiClient: APIClientProtocol {
get { self[APIClientKey.self] }
set { self[APIClientKey.self] = newValue }
}
}
// Usage
struct PostListView: View {
@Environment(\.apiClient) private var apiClient
@State private var posts: [Post] = []
var body: some View {
List(posts) { post in PostRowView(post: post) }
.task { posts = (try? await apiClient.fetchPosts()) ?? [] }
}
}
// Mock in previews
#Preview { PostListView().environment(\.apiClient, MockAPIClient()) }
enum Route: Hashable {
case settings
case profile(userId: String)
case notifications
}
struct MainView: View {
@State private var navigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $navigationPath) {
HomeView(navigationPath: $navigationPath)
.navigationDestination(for: Route.self) { route in
switch route {
case .settings: SettingsView()
case .profile(let id): ProfileView(userId: id)
case .notifications: NotificationsView()
}
}
}
}
}
struct MainTabView: View {
@State private var selectedTab = Tab.home
enum Tab: String, CaseIterable {
case home, search, profile
var icon: String {
switch self { case .home: "house"; case .search: "magnifyingglass"; case .profile: "person" }
}
var title: String { rawValue.capitalized }
}
var body: some View {
TabView(selection: $selectedTab) {
ForEach(Tab.allCases, id: \.self) { tab in
NavigationStack { tabContent(for: tab) }
.tabItem { Label(tab.title, systemImage: tab.icon) }
.tag(tab)
}
}
}
@ViewBuilder
private func tabContent(for tab: Tab) -> some View {
switch tab { case .home: HomeView(); case .search: SearchView(); case .profile: ProfileView() }
}
}
struct PostListView: View {
@State private var posts: [Post] = []
@State private var searchText = ""
var filteredPosts: [Post] {
guard !searchText.isEmpty else { return posts }
return posts.filter { $0.title.localizedCaseInsensitiveContains(searchText) }
}
var body: some View {
List {
ForEach(filteredPosts) { post in
PostRowView(post: post)
.swipeActions(edge: .trailing) {
Button(role: .destructive) { deletePost(post) } label: {
Label("Delete", systemImage: "trash")
}
}
}
}
.searchable(text: $searchText, prompt: "Search posts")
.refreshable { await loadPosts() }
.overlay {
if filteredPosts.isEmpty { ContentUnavailableView.search(text: searchText) }
}
.task { await loadPosts() }
}
private func loadPosts() async { /* fetch */ }
private func deletePost(_ post: Post) { posts.removeAll { $0.id == post.id } }
}
For photo grids, use LazyVGrid with GridItem(.adaptive(minimum:maximum:)). See references/patterns.md for grid and horizontal scroll patterns.
struct RegistrationView: View {
@State private var form = RegistrationForm()
@State private var isSubmitting = false
var body: some View {
Form {
Section("Personal Information") {
TextField("Full Name", text: $form.name).textContentType(.name)
TextField("Email", text: $form.email).textContentType(.emailAddress).autocapitalization(.none)
}
Section("Account") {
SecureField("Password", text: $form.password)
SecureField("Confirm", text: $form.confirmPassword)
if !form.passwordsMatch && !form.confirmPassword.isEmpty {
Text("Passwords do not match").font(.caption).foregroundStyle(.red)
}
}
Section {
Button("Create Account") { Task { await submit() } }
.disabled(!form.isValid || isSubmitting)
}
}
.loading(isSubmitting)
}
private func submit() async { /* validate and submit */ }
}
#Preview("User Profile") {
NavigationStack { UserProfileView(user: .preview) }
}
#Preview("Dark Mode") {
NavigationStack { UserProfileView(user: .preview) }.preferredColorScheme(.dark)
}
extension User {
static var preview: User { User(id: "preview", name: "John Doe", email: "[email protected]") }
}
XCTest for unit and UI testsasync throws test methodstest_loadUser_success_updatesUser()final class UserViewModelTests: XCTestCase {
var sut: UserViewModel!
var mockService: MockUserService!
override func setUp() {
super.setUp(); mockService = MockUserService()
sut = UserViewModel(userService: mockService)
}
func test_loadUser_success_updatesUser() async {
mockService.userToReturn = User(id: "1", name: "John")
await sut.loadUser(id: "1")
XCTAssertEqual(sut.user?.name, "John")
XCTAssertFalse(sut.isLoading)
}
func test_loadUser_failure_setsErrorMessage() async {
mockService.errorToThrow = APIError.invalidResponse
await sut.loadUser(id: "1")
XCTAssertNil(sut.user)
XCTAssertNotNil(sut.errorMessage)
}
}
xcodebuild -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15' # Build
xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15' # Test
swift build # SPM build
swift test # SPM test
swift format . # Format
swiftlint # Lint
swiftlint --fix # Auto-fix
Do: Decompose views with computed properties | Use @Observable (iOS 17+) | Inject via @Environment | Use .task for async | Use NavigationStack with typed routes | Provide multiple #Preview configs | Use LazyVStack/LazyHStack for scrollable content
Don't: Mutate state during view updates | Create @StateObject in computed properties | Use @ObservedObject for owned state | Put heavy computation in body | Force-unwrap outside tests | Use AnyView (use @ViewBuilder) | Nest views 4+ levels deep without extraction
For detailed patterns and examples, see:
development
Zig language guardrails, patterns, and best practices for AI-assisted development. Use when working with Zig files (.zig), build.zig, or when the user mentions Zig. Provides comptime patterns, allocator conventions, C interop guidelines, and testing standards specific to this project's coding standards.
tools
WordPress framework guardrails, patterns, and best practices for AI-assisted development. Use when working with WordPress projects, or when the user mentions WordPress. Provides theme development, plugin architecture, REST API, blocks, and security guidelines.
tools
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs. Use when testing web apps, automating browser interactions, or debugging frontend issues.
tools
Suite of tools for creating elaborate, multi-component web applications using modern frontend technologies (React, Tailwind CSS, shadcn/ui). Use for complex projects requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX pages.