Install
$ agentstack add skill-abdullah4ai-apple-developer-toolkit-ios-rules ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
iOS Development Rules
38 rules for writing production-quality iOS apps. Each rule targets common LLM mistakes with concrete fixes.
Accessibility
REDUCE MOTION:
- Check: @Environment(\.accessibilityReduceMotion) var reduceMotion
- When enabled:
- Replace .spring() with .easeInOut(duration: 0.2)
- Replace slide transitions with .opacity
- Disable auto-playing animations
- Keep functional animations (progress bars), remove decorative ones
withAnimation(reduceMotion ? .easeInOut(duration: 0.2) : .spring(response: 0.3)) {
// state change
}
.transition(reduceMotion ? .opacity : .slide)
REDUCE TRANSPARENCY:
- Check: @Environment(\.accessibilityReduceTransparency) var reduceTransparency
- When enabled: use opaque backgrounds instead of materials/blur.
.background(reduceTransparency ? Color(AppTheme.Colors.surface) : .ultraThinMaterial)
VOICEOVER LABELS:
- All interactive elements: .accessibilityLabel("descriptive text").
- Non-obvious actions: .accessibilityHint("Double tap to delete this item").
- Decorative images: .accessibilityHidden(true).
- Informative images: .accessibilityLabel("Profile photo of John").
- Icon-only buttons: MUST have .accessibilityLabel().
Button(action: addItem) {
Image(systemName: "plus")
}
.accessibilityLabel("Add new item")
GROUPING & COMBINING:
- Related content (icon + label + value): .accessibilityElement(children: .combine).
- Custom read order: .accessibilityElement(children: .ignore) + manual .accessibilityLabel.
- Cards with multiple elements: combine into single accessible element.
HStack {
Image(systemName: "heart.fill")
Text("Favorites")
Spacer()
Text("12")
}
.accessibilityElement(children: .combine)
ACCESSIBILITY TRAITS:
- Section headers: .accessibilityAddTraits(.isHeader)
- Buttons that play media: .accessibilityAddTraits(.startsMediaSession)
- Summary/aggregate values: .accessibilityAddTraits(.isSummaryElement)
- Selected items: .accessibilityAddTraits(.isSelected)
FOCUS MANAGEMENT:
- Use @FocusState with field enum for form navigation.
- .submitLabel(.next) to show "Next" on keyboard, .submitLabel(.done) for last field.
- Chain fields with .onSubmit { focusedField = .nextField }.
enum Field: Hashable { case name, email, password }
@FocusState private var focusedField: Field?
TextField("Name", text: $name)
.focused($focusedField, equals: .name)
.submitLabel(.next)
.onSubmit { focusedField = .email }
DYNAMIC TYPE:
- System text styles (.body, .headline, etc.) scale automatically.
- NEVER use .font(.system(size:)) — it opts out of Dynamic Type.
- If layout breaks at large sizes: .minimumScaleFactor(0.8) as last resort.
- Test with Xcode Environment Overrides at the largest accessibility size.
- ScrollView wraps content that may overflow at large type sizes.
COLOR & CONTRAST:
- Don't use color alone for status — always pair with icon + text.
- Minimum 4.5:1 contrast for normal text, 3:1 for large text.
- .foregroundStyle(.secondary) for de-emphasized text (maintains adaptive contrast).
ACCESSIBLE CUSTOM CONTROLS:
- Custom sliders/steppers: .accessibilityValue(), .accessibilityAdjustableAction().
- Custom toggles: .accessibilityAddTraits(.isToggle), .accessibilityValue(isOn ? "on" : "off").
- Progress indicators: .accessibilityValue("\(Int(progress * 100)) percent").
App Clips
APP CLIPS: SETUP: Requires separate App Clip target (kind: "app_clip" in plan extensions array). App Clips are a lightweight version of your app for quick, focused tasks.
INFO.PLIST (auto-configured on App Clip target in project.yml): NSAppClip dict with NSAppClipRequestEphemeralUserNotification and NSAppClipRequestLocationConfirmation is set automatically. No manual configuration needed.
ASSOCIATED DOMAINS (auto-configured in project.yml entitlements): appclips:{bundleID} and parent-application-identifiers are set automatically.
APP CLIP EXPERIENCE URL: Configure in App Store Connect. Users launch App Clip via NFC, QR code, Maps, etc.
APP CLIP INVOCATION (receive URL): struct AppClipApp: App { var body: some Scene { WindowGroup { ContentView() .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in guard let url = activity.webpageURL else { return } // Handle URL: extract parameters, show relevant content } } } }
SKOverlay (promote full app from within App Clip): import StoreKit @Environment(\.requestAppStoreOverlay) var requestOverlay Button("Get Full App") { requestOverlay(AppStoreOverlay.AppClipCompletion(appIdentifier: "YOURAPPID")) }
CONSTRAINTS:
- App Clip binary must be = 5, key task completed)
- Track with @AppStorage("launchCount"), increment in .onAppear of root view
- Apple limits to 3 prompts/year — never on first launch
- Check: never request immediately after error, crash, or purchase
Apple Translation
APPLE ON-DEVICE TRANSLATION (Translation framework): FRAMEWORK: import Translation (iOS 17.4+, on-device, NO internet required, NO API key)
KEY DISTINCTION: Translation is for translating USER CONTENT on demand (e.g. translating a message from French to English). It is NOT for app localization (.strings files). Do not confuse the two.
MODIFIER APPROACH (simplest — shows system translation sheet): @State private var showTranslation = false Text(userContent) .translationPresentation(isPresented: $showTranslation, text: userContent) Button("Translate") { showTranslation = true }
PROGRAMMATIC TRANSLATION (TranslationSession): @State private var translatedText = ""
func translateText(_ input: String) async { let config = TranslationSession.Configuration(source: .init(identifier: "fr"), target: .init(identifier: "en")) let session = TranslationSession(configuration: config) do { let response = try await session.translate(input) translatedText = response.targetText } catch { // Handle: language pair not supported on device, model not downloaded } }
// Call with .task or Button: .task { await translateText(originalText) }
SUPPORTED LANGUAGES: Check Translation.supportedLanguages for the device's available language pairs. AVAILABILITY: Some language pairs require a model download on first use. NO ENTITLEMENTS NEEDED: Translation framework requires no special entitlements or Info.plist keys.
Biometrics
BIOMETRIC AUTHENTICATION (Face ID / Touch ID):
- import LocalAuthentication; LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics)
- Requires NSFaceIDUsageDescription permission (add CONFIG_CHANGES)
- Check canEvaluatePolicy first; fall back to passcode if biometrics unavailable
- LAContext().biometryType to detect .faceID vs .touchID vs .none
- Always provide manual unlock alternative (PIN/password)
Camera
CAMERA & PHOTOS:
- PhotosPicker (PhotosUI) for gallery selection — no permissions needed for limited access
- For camera capture: AVCaptureSession + AVCapturePhotoOutput + UIViewControllerRepresentable wrapper
- Camera requires NSCameraUsageDescription permission (add CONFIG_CHANGES)
- Full photo library requires NSPhotoLibraryUsageDescription
- Use @State private var selectedItem: PhotosPickerItem? with .onChange to load
- Load image: try await item.loadTransferable(type: Data.self)
Charts
SWIFT CHARTS:
- import Charts; use Chart { } container
- BarMark, LineMark, AreaMark, PointMark, RuleMark for data visualization
- .foregroundStyle(by: .value("Category", item.category)) for color coding
- chartXAxis { AxisMarks() }, chartYAxis { AxisMarks() } for custom axis labels
- Extract Chart into a separate computed property to avoid body complexity
- Use .chartScrollableAxes(.horizontal) for large datasets
Color & Contrast
CONTRAST RATIO REQUIREMENTS (WCAG 2.1 / Apple HIG):
- Normal text ( 300ms. Instant operations need no indicator.
BADGE/CHIP PATTERN:
Text("Label")
.font(.caption)
.fontWeight(.medium)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(AppTheme.Colors.primary.opacity(0.15))
.foregroundStyle(AppTheme.Colors.primary)
.clipShape(Capsule())
TOGGLE/SWITCH:
- Use Toggle for binary settings with immediate effect.
- Label must clearly describe the ON state.
- Group related toggles in a Section with a header.
PICKER PATTERNS:
- 2-4 options: Picker with .segmentedStyle.
- 5+ options: Picker with default menu style or NavigationLink to selection list.
- Date selection: DatePicker with appropriate displayedComponents.
EMPTY STATES:
- Always use ContentUnavailableView for empty lists/collections.
- Include: icon (SF Symbol), title, description, and action button if applicable.
- Never show a blank screen — empty state guides the user to the first action.
DIVIDERS:
- Use sparingly — prefer spacing to create visual separation.
- In lists: SwiftUI List provides dividers automatically.
- Custom dividers: Divider() with .padding(.horizontal) for inset style.
Dark Mode
DARK/LIGHT MODE:
- 3-way picker (system/light/dark) is the standard pattern:
@AppStorage("appearance") private var appearance: String = "system" private var preferredColorScheme: ColorScheme? { switch appearance { case "light": return .light; case "dark": return .dark; default: return nil } } .preferredColorScheme(preferredColorScheme) // on outermost container in @main app
- CRITICAL: .preferredColorScheme() MUST be in the root @main app, NOT just in the settings view.
- System option: .preferredColorScheme(nil) follows device setting.
- Settings screen: Picker with light/dark/system options writing to @AppStorage("appearance").
ADAPTIVE THEME COLORS (no color assets needed):
- Switch ALL AppTheme palette colors from plain Color(hex:) to Color(light:dark:) with TWO hex values:
static let background = Color(light: Color(hex: "#F8F9FA"), dark: Color(hex: "#1C1C1E")) static let surface = Color(light: Color(hex: "#FFFFFF"), dark: Color(hex: "#2C2C2E"))
- Color(light:dark:) uses UIColor(dynamicProvider:) — reacts to .preferredColorScheme() automatically.
- YOU decide the dark palette based on app mood — user does not specify dark colors.
- Dark palette guidelines: darken backgrounds (#1C1C1E, #2C2C2E), lighten/brighten accents slightly, use Color.primary/Color.secondary for text.
- AppTheme MUST include the Color(light:dark:) extension (see shared constraints).
Design System Rules
AppTheme Pattern
Every app MUST use a centralized theme with nested enums for Colors, Fonts, and Spacing. Do NOT use a flat enum with top-level static properties.
// REQUIRED — always use nested enums
import SwiftUI
enum AppTheme {
enum Colors {
static let accent = Color.blue // one accent per app
static let textPrimary = Color.primary
static let textSecondary = Color.secondary
static let background = Color(.systemBackground)
static let surface = Color(.secondarySystemBackground)
static let cardBackground = Color(.secondarySystemGroupedBackground)
}
enum Fonts {
static let largeTitle = Font.largeTitle
static let title = Font.title
static let headline = Font.headline
static let body = Font.body
static let caption = Font.caption
}
enum Spacing {
static let small: CGFloat = 8
static let medium: CGFloat = 16
static let large: CGFloat = 24
static let cornerRadius: CGFloat = 12
}
}
// FORBIDDEN — never use flat structure
enum AppTheme {
static let accentColor = Color.blue // ❌ wrong
static let spacing: CGFloat = 8 // ❌ wrong
}
Reference as: AppTheme.Colors.accent, AppTheme.Fonts.headline, AppTheme.Spacing.medium
Typography
- System fonts only — use SwiftUI font styles:
.largeTitle,.title,.headline,.body,.caption - No custom fonts, no downloaded fonts
- Use
AppTheme.Fontsfor consistent sizing
Icons (SF Symbols)
- SF Symbols only for all icons — required for every list row, button, empty state, and tab
- Reference via
Image(systemName: "symbol.name") - Pick domain-appropriate symbols (e.g. "checkmark.circle.fill" for todos, "note.text" for notes, "heart.fill" for favorites)
- Use
.symbolRenderingMode(.hierarchical)or.symbolRenderingMode(.palette)for visual depth - No custom icon assets unless the app concept specifically requires them
Colors
- One accent color that fits the app's purpose
- Use semantic colors:
.primary,.secondary,Color(.systemBackground) - Do NOT add dark mode support, colorScheme checks, or custom dark/light color handling unless the user explicitly requests it
Spacing Standards
- 16pt standard padding (outer margins, section spacing)
- 8pt compact spacing (between related elements)
- 24pt large spacing (between major sections)
- Use
AppTheme.Spacingconstants throughout
Empty States
Every list or collection MUST have an empty state. Use ContentUnavailableView (iOS 17+) for a polished look:
// Required — show when collection is empty
if items.isEmpty {
ContentUnavailableView(
"No Notes Yet",
systemImage: "note.text",
description: Text("Tap + to create your first note")
)
} else {
// Show the list
}
For custom empty states, use a styled VStack with SF Symbol + descriptive text:
VStack(spacing: 16) {
Image(systemName: "tray")
.font(.system(size: 48))
.foregroundStyle(.secondary)
Text("Nothing here yet")
.font(.title3)
Text("Add your first item to get started")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Animations
Use subtle, purposeful animations for state changes and list mutations:
// Toggle/complete actions — spring animation
withAnimation(.spring) {
item.isComplete.toggle()
}
// List insertions/removals — combine opacity + scale
.transition(.opacity.combined(with: .scale))
// Numeric text changes
.contentTransition(.numericText())
// Filter/tab changes
.animation(.default, value: selectedFilter)
Rules:
- Always use
withAnimation(.spring)for toggle/complete state changes - Always add
.transition(.opacity.combined(with: .scale))for list add/remove - Never add gratuitous motion that slows down interaction
- Keep animations subtle —
.springand.defaultcurves only
Feedback States
LOADING PATTERNS:
- Inline button spinner (action on single element):
Button {
Task { await save() }
} label: {
if isSaving {
ProgressView()
.controlSize(.small)
} else {
Text("Save")
}
}
.disabled(isSaving)
- Full-screen loading (initial data load):
if isLoading {
ProgressView("Loading...")
} else {
ContentView()
}
- Skeleton loading (content placeholders):
ForEach(Item.sampleData) { item in
ItemRow(item: item)
}
.redacted(reason: .placeholder)
- Pull-to-refresh (list content):
List { ... }
.refreshable { await viewModel.refresh() }
- Overlay loading (blocking operation):
.overlay {
if isProcessing {
ZStack {
Color.black.opacity(0.3)
ProgressView()
.controlSize(.large)
.tint(.white)
}
.ignoresSafeArea()
}
}
LOADING RULES:
- Show indicator for operations > 300ms.
- ALWAYS disable the triggering button while loading (prevents double-taps).
- Never block the entire UI for a partial operation — use inline spinner.
- Match loading style to scope: button-level → inline, screen-level → full-screen.
ERROR HANDLING UI:
- Inline validation (below form fields):
if let error = emailError {
HStack(spacing: 4) {
Image(systemName: "exclamationmark.circle.fill")
Text(error)
}
.font(.caption)
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [Abdullah4AI](https://github.com/Abdullah4AI)
- **Source:** [Abdullah4AI/apple-developer-toolkit](https://github.com/Abdullah4AI/apple-developer-toolkit)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.