swiftship/internal/skills/data/ui/feedback-states/SKILL.md
Feedback state patterns: loading indicators, error handling UI, success confirmations, disabled states, skeleton views. Use when adding loading spinners, error alerts, success feedback, or managing empty/error/loading view states. Triggers: ProgressView, loading, error, alert, empty state, skeleton, disabled, feedback.
npx skillsauth add abdullah4ai/apple-dev-docs feedback-statesInstall 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.
Every view displaying async data MUST use a switch on Loadable<T> covering ALL 4 states. This is not optional.
switch viewModel.items {
case .notInitiated, .loading:
ProgressView("Loading...")
case .success(let items) where items.isEmpty:
ContentUnavailableView("No Items Yet", systemImage: "tray",
description: Text("Tap + to create your first item"))
case .success(let items):
List(items) { item in ItemRow(item: item) }
case .failure(let error):
ContentUnavailableView {
Label("Load Failed", systemImage: "exclamationmark.triangle")
} description: {
Text(error.localizedDescription)
} actions: {
Button("Retry") { Task { await viewModel.loadItems() } }
}
}
Rules:
if let to unwrap only the success case — all 4 states must be handledContentUnavailableView with an action buttonProgressViewEvery mutation button (save, delete, upload, send) MUST:
Button {
Task { await viewModel.save() }
} label: {
if viewModel.saveState.isLoading {
ProgressView()
.controlSize(.small)
} else {
Text("Save")
}
}
.disabled(viewModel.saveState.isLoading)
LOADING PATTERNS:
Button {
Task { await save() }
} label: {
if saveState.isLoading {
ProgressView()
.controlSize(.small)
} else {
Text("Save")
}
}
.disabled(saveState.isLoading)
switch viewModel.items {
case .notInitiated, .loading:
ProgressView("Loading...")
case .success(let items):
ContentListView(items: items)
case .failure(let error):
ErrorView(error: error) {
Task { await viewModel.loadItems() }
}
}
ForEach(Item.sampleData) { item in
ItemRow(item: item)
}
.redacted(reason: .placeholder)
List { ... }
.refreshable { await viewModel.refresh() }
.overlay {
if operationState.isLoading {
ZStack {
Color.black.opacity(0.3)
ProgressView()
.controlSize(.large)
.tint(.white)
}
.ignoresSafeArea()
}
}
LOADING RULES:
Loadable<T> for all async state — never var isLoading: Bool + var errorMessage: String?.ERROR HANDLING UI:
if let error = emailError {
HStack(spacing: 4) {
Image(systemName: "exclamationmark.circle.fill")
Text(error)
}
.font(AppTheme.Fonts.caption)
.foregroundStyle(AppTheme.Colors.error)
}
.alert("Error", isPresented: $showError) {
Button("Retry") { Task { await retry() } }
Button("Cancel", role: .cancel) { }
} message: {
Text(errorMessage)
}
if let error = bannerError {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(AppTheme.Colors.warning)
Text(error)
.font(AppTheme.Fonts.subheadline)
Spacer()
Button("Dismiss") { bannerError = nil }
.font(AppTheme.Fonts.caption)
}
.padding(AppTheme.Spacing.small)
.background(.orange.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 8))
.padding(.horizontal, AppTheme.Spacing.medium)
}
ERROR HANDLING RULES:
SUCCESS FEEDBACK:
// Brief success animation
withAnimation(.spring(response: 0.3)) {
showSuccess = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation { showSuccess = false }
}
DISABLED STATE:
NETWORK/SYSTEM ERROR PATTERN (ViewModel with Loadable):
@MainActor @Observable
class ItemViewModel {
var items: Loadable<[Item]> = .notInitiated
func loadItems() async {
items = .loading
do {
items = .success(try await fetchItems())
} catch {
items = .failure(error)
}
}
var userFacingError: String? {
if case .failure = items {
return "Couldn't load items. Pull to refresh to try again."
}
return nil
}
}
EMPTY VS ERROR VS LOADING (switch on Loadable):
switch viewModel.items {
case .notInitiated, .loading:
ProgressView("Loading...")
case .success(let items) where items.isEmpty:
ContentUnavailableView("No Items Yet", systemImage: "tray",
description: Text("Tap + to create your first item"))
case .success(let items):
List(items) { item in ItemRow(item: item) }
case .failure(let error):
ContentUnavailableView {
Label("Load Failed", systemImage: "exclamationmark.triangle")
} description: {
Text(error.localizedDescription)
} actions: {
Button("Retry") { Task { await viewModel.loadItems() } }
}
}
Loadable<T> makes each state explicit and compiler-enforced via switch.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.