# Ios Rules

> 38 battle-tested iOS development rules covering accessibility, navigation, architecture, dark mode, localization, App Review guidelines, and more. Targets the mistakes LLMs actually make when generating Swift/SwiftUI code.

- **Type:** Skill
- **Install:** `agentstack add skill-abdullah4ai-apple-developer-toolkit-ios-rules`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Abdullah4AI](https://agentstack.voostack.com/s/abdullah4ai)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Abdullah4AI](https://github.com/Abdullah4AI)
- **Source:** https://github.com/Abdullah4AI/apple-developer-toolkit/tree/main/skills/ios-rules

## Install

```sh
agentstack add skill-abdullah4ai-apple-developer-toolkit-ios-rules
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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
```swift
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.
```swift
.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().
```swift
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.
```swift
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 }.
```swift
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: "YOUR_APP_ID"))
}

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:
```swift
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.

```swift
// 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
    }
}
```

```swift
// 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.Fonts` for 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.Spacing` constants throughout

## Empty States
Every list or collection MUST have an empty state. Use `ContentUnavailableView` (iOS 17+) for a polished look:

```swift
// 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:

```swift
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:

```swift
// 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 — `.spring` and `.default` curves only

# Feedback States

LOADING PATTERNS:

1. Inline button spinner (action on single element):
```swift
Button {
    Task { await save() }
} label: {
    if isSaving {
        ProgressView()
            .controlSize(.small)
    } else {
        Text("Save")
    }
}
.disabled(isSaving)
```

2. Full-screen loading (initial data load):
```swift
if isLoading {
    ProgressView("Loading...")
} else {
    ContentView()
}
```

3. Skeleton loading (content placeholders):
```swift
ForEach(Item.sampleData) { item in
    ItemRow(item: item)
}
.redacted(reason: .placeholder)
```

4. Pull-to-refresh (list content):
```swift
List { ... }
    .refreshable { await viewModel.refresh() }
```

5. Overlay loading (blocking operation):
```swift
.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:

1. Inline validation (below form fields):
```swift
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.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-abdullah4ai-apple-developer-toolkit-ios-rules
- Seller: https://agentstack.voostack.com/s/abdullah4ai
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
