agentleFS
travisjneumantravisjneuman/.claude/skills/ios-development/SKILL.md

ios-development

Skill106 starsChanged 9 months agoMITView on GitHub

iOS, iPadOS, and tvOS development with Swift, SwiftUI, and UIKit. Use when building Apple platform apps, implementing iOS-specific features, or following Apple Human Interface Guidelines.

5 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

2 rules. 0 are common, 2 are distinctive, and 0 are very specific to this project.

  • 0

    Common

    Most of the 5 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use SwiftUI for new projects

    line 452
  2. Distinctive:

    Use Swift's type system fully

    line 456

“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

iOS/iPadOS/tvOS Development

Comprehensive guide for building native Apple platform applications.

Platforms Covered

PlatformMinimum TargetCurrent
iOSiOS 15.0+iOS 17+
iPadOSiPadOS 15.0+iPadOS 17+
tvOStvOS 15.0+tvOS 17+
watchOSwatchOS 8.0+watchOS 10+

SwiftUI (Preferred for New Projects)

Basic Structure

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 20) {
            Text("Count: \(count)")
                .font(.largeTitle)

            Button("Increment") {
                count += 1
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

State Management

// Local state
@State private var text = ""

// Binding (child components)
@Binding var isPresented: Bool

// Observable object
@StateObject private var viewModel = MyViewModel()
@ObservedObject var viewModel: MyViewModel

// Environment
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
@EnvironmentObject var appState: AppState

// App Storage (UserDefaults)
@AppStorage("username") private var username = ""

Modern Concurrency

// Async/await
func fetchData() async throws -> [Item] {
    let url = URL(string: "https://api.example.com/items")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode([Item].self, from: data)
}

// In View
.task {
    do {
        items = try await fetchData()
    } catch {
        errorMessage = error.localizedDescription
    }
}

// Main actor for UI updates
@MainActor
class ViewModel: ObservableObject {
    @Published var items: [Item] = []

    func load() async {
        items = try? await fetchData()
    }
}

Navigation (iOS 16+)

// NavigationStack with typed destinations
struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List(items) { item in
                NavigationLink(value: item) {
                    Text(item.name)
                }
            }
            .navigationDestination(for: Item.self) { item in
                DetailView(item: item)
            }
        }
    }
}

Lists and Grids

// Modern List
List {
    ForEach(items) { item in
        ItemRow(item: item)
    }
    .onDelete(perform: delete)
    .onMove(perform: move)
}
.listStyle(.insetGrouped)
.searchable(text: $searchText)
.refreshable {
    await refresh()
}

// LazyVGrid
LazyVGrid(columns: [
    GridItem(.adaptive(minimum: 150))
]) {
    ForEach(items) { item in
        ItemCard(item: item)
    }
}

UIKit (Legacy/Complex UI)

View Controller Lifecycle

class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // About to appear
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // Fully visible
    }
}

Auto Layout

// Programmatic constraints
NSLayoutConstraint.activate([
    label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
    label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
    label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
])

Data Persistence

Core Data

// Model
@objc(Item)
public class Item: NSManagedObject {
    @NSManaged public var id: UUID
    @NSManaged public var name: String
    @NSManaged public var createdAt: Date
}

// Fetch
@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \Item.createdAt, ascending: false)],
    animation: .default
)
private var items: FetchedResults<Item>

SwiftData (iOS 17+)

@Model
class Item {
    var id: UUID
    var name: String
    var createdAt: Date

    init(name: String) {
        self.id = UUID()
        self.name = name
        self.createdAt = Date()
    }
}

// In View
@Query(sort: \Item.createdAt, order: .reverse)
private var items: [Item]

@Environment(\.modelContext) private var modelContext

func addItem() {
    let item = Item(name: "New Item")
    modelContext.insert(item)
}

Keychain

// Store sensitive data
import Security

func saveToKeychain(key: String, data: Data) throws {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: key,
        kSecValueData as String: data
    ]
    SecItemDelete(query as CFDictionary)
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else {
        throw KeychainError.saveFailed
    }
}

Networking

URLSession

actor NetworkService {
    func fetch<T: Decodable>(_ type: T.Type, from url: URL) async throws -> T {
        let (data, response) = try await URLSession.shared.data(from: url)

        guard let httpResponse = response as? HTTPURLResponse,
              (200...299).contains(httpResponse.statusCode) else {
            throw NetworkError.invalidResponse
        }

        return try JSONDecoder().decode(T.self, from: data)
    }
}

Apple Human Interface Guidelines

iOS Design Principles

  • Clarity: Text is legible, icons precise, adornments subtle
  • Deference: Content is paramount, UI doesn't compete
  • Depth: Visual layers and realistic motion convey hierarchy

Key Metrics

ElementSize
Touch target44x44 pt minimum
Navigation bar44 pt
Tab bar49 pt
Status bar47 pt (notch) / 20 pt (legacy)

Safe Areas

.safeAreaInset(edge: .bottom) {
    BottomBar()
}

// Ignore safe area
.ignoresSafeArea(.keyboard)
.ignoresSafeArea(.container, edges: .bottom)

Platform-Specific

iPadOS Specifics

// Split view
NavigationSplitView {
    Sidebar()
} content: {
    ContentList()
} detail: {
    DetailView()
}

// Keyboard shortcuts
.keyboardShortcut("n", modifiers: .command)

// Hover effects
.hoverEffect(.highlight)

tvOS Specifics

// Focus management
@FocusState private var isFocused: Bool

Button("Action") { }
    .focused($isFocused)
    .focusable()

// Large text for 10-foot UI
.font(.system(size: 38))

App Store Requirements

Required Assets

  • App icon (1024x1024)
  • Screenshots for each device size
  • Privacy policy URL
  • App description (4000 char max)

Privacy

<!-- Info.plist -->
<key>NSCameraUsageDescription</key>
<string>We need camera access to...</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need photo access to...</string>

App Transport Security

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
</dict>

Testing

Unit Tests

import XCTest
@testable import MyApp

final class MyTests: XCTestCase {
    func testExample() async throws {
        let sut = MyViewModel()
        await sut.load()
        XCTAssertEqual(sut.items.count, 10)
    }
}

UI Tests

import XCTest

final class MyUITests: XCTestCase {
    let app = XCUIApplication()

    override func setUp() {
        continueAfterFailure = false
        app.launch()
    }

    func testNavigation() {
        app.buttons["Start"].tap()
        XCTAssertTrue(app.staticTexts["Welcome"].exists)
    }
}

Project Structure

MyApp/
├── App/
│   └── MyApp.swift
├── Features/
│   ├── Home/
│   │   ├── HomeView.swift
│   │   └── HomeViewModel.swift
│   └── Settings/
│       └── SettingsView.swift
├── Core/
│   ├── Models/
│   ├── Services/
│   └── Extensions/
├── Resources/
│   ├── Assets.xcassets
│   └── Localizable.strings
└── Tests/

Best Practices

DO:

  • Use SwiftUI for new projects
  • Support Dynamic Type for accessibility
  • Handle all device orientations
  • Implement proper error handling
  • Use Swift's type system fully

DON'T:

  • Force unwrap optionals without validation
  • Block main thread with network calls
  • Hardcode strings (use localization)
  • Ignore memory management (retain cycles)
  • Skip accessibility labels

@Observable Macro (iOS 17+, Replacing ObservableObject)

import Observation

// New pattern: @Observable macro (replaces ObservableObject + @Published)
@Observable
class UserViewModel {
    var user: User?
    var isLoading = false
    var errorMessage: String?

    func load() async {
        isLoading = true
        defer { isLoading = false }
        do {
            user = try await fetchUser()
        } catch {
            errorMessage = error.localizedDescription
        }
    }
}

// In View: no @StateObject/@ObservedObject wrappers needed
struct UserView: View {
    @State private var viewModel = UserViewModel()

    var body: some View {
        Group {
            if viewModel.isLoading {
                ProgressView()
            } else if let user = viewModel.user {
                Text(user.name)
            }
        }
        .task { await viewModel.load() }
    }
}

// @Bindable for two-way bindings
struct EditView: View {
    @Bindable var viewModel: EditViewModel

    var body: some View {
        TextField("Name", text: $viewModel.name)
    }
}

Migration from ObservableObject

Old (ObservableObject)New (@Observable)
class: ObservableObject@Observable class
@Published varvar (automatic)
@StateObject@State
@ObservedObjectDirect reference
@EnvironmentObject@Environment(TypeName.self)

SwiftUI 6 New Views and Modifiers

// Mesh gradient (iOS 18+)
MeshGradient(
    width: 3, height: 3,
    points: [
        .init(0, 0), .init(0.5, 0), .init(1, 0),
        .init(0, 0.5), .init(0.5, 0.5), .init(1, 0.5),
        .init(0, 1), .init(0.5, 1), .init(1, 1)
    ],
    colors: [
        .red, .purple, .indigo,
        .orange, .cyan, .blue,
        .yellow, .green, .mint
    ]
)

// Zoom navigation transition
NavigationLink {
    DetailView(item: item)
} label: {
    ItemCard(item: item)
}
.matchedTransitionSource(id: item.id, in: namespace)

// ScrollView enhancements
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemRow(item: item)
        }
    }
}
.scrollPosition(id: $scrolledID)
.defaultScrollAnchor(.bottom)

// Custom containers
@Entry macro for EnvironmentValues
@Environment(\.myCustomValue) var value

Swift 6 Concurrency

// Complete data race safety (strict concurrency checking)
// Enable in Xcode: SWIFT_STRICT_CONCURRENCY = complete

// Sendable conformance required for data crossing isolation boundaries
struct UserData: Sendable {
    let id: UUID
    let name: String
}

// Actor isolation
actor DatabaseManager {
    private var cache: [String: Data] = [:]

    func get(_ key: String) -> Data? { cache[key] }
    func set(_ key: String, value: Data) { cache[key] = value }
}

// Global actors
@MainActor
class ViewModel {
    var items: [Item] = []

    func load() async {
        let data = await fetchFromNetwork() // Runs off main actor
        items = data // Back on main actor automatically
    }
}

// Typed throws (Swift 6)
enum DataError: Error {
    case notFound, corrupted
}

func loadData() throws(DataError) -> Data {
    guard let data = cache.get("key") else {
        throw .notFound
    }
    return data
}

Minimum Target: iOS 17+

For new projects, target iOS 17+ to access:

FeatureMinimum iOSBenefit
@Observable17Simpler state management
SwiftData17Modern persistence (replaces Core Data)
NavigationStack16Type-safe navigation
Swift Charts16Native charting
MeshGradient18Beautiful gradients
ControlWidget18Control Center widgets

visionOS and Spatial Computing

// visionOS app structure
@main
struct MyVisionApp: App {
    var body: some Scene {
        // 2D window
        WindowGroup {
            ContentView()
        }

        // Immersive space
        ImmersiveSpace(id: "immersiveScene") {
            ImmersiveView()
        }

        // 3D volume
        WindowGroup(id: "3dContent") {
            Model3D(named: "Globe") { model in
                model.resizable()
                    .aspectRatio(contentMode: .fit)
            } placeholder: {
                ProgressView()
            }
        }
        .windowStyle(.volumetric)
    }
}

// RealityKit integration
import RealityKit

struct ImmersiveView: View {
    var body: some View {
        RealityView { content in
            let sphere = MeshResource.generateSphere(radius: 0.5)
            let material = SimpleMaterial(color: .blue, isMetallic: true)
            let entity = ModelEntity(mesh: sphere, materials: [material])
            content.add(entity)
        }
        .gesture(TapGesture().targetedToAnyEntity().onEnded { value in
            // Handle spatial tap
        })
    }
}

watchOS Development

// watchOS app with SwiftUI
@main
struct MyWatchApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationStack {
                ContentView()
            }
        }
    }
}

// Complications / Widgets
struct MyWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: "myWidget", provider: Provider()) { entry in
            MyWidgetView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .supportedFamilies([
            .accessoryCircular,
            .accessoryRectangular,
            .accessoryInline,
        ])
    }
}

// Watch Connectivity for iPhone <-> Watch communication
import WatchConnectivity

class ConnectivityManager: NSObject, WCSessionDelegate {
    func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
        // Handle message from paired device
    }
}

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.