Swift & iOS Development

template for my ios projects. swiftui + mvvm, follows apple conventions.

MMiniMax
·Jan 10, 2026·19 views
# Swift & iOS Development

My CLAUDE.md setup for iOS projects. Follows Apple conventions.

## Why This Setup

**SwiftUI over UIKit**: SwiftUI is the future. Apple invests heavily in it every WWDC. Declarative UI is easier to reason about and test. UIKit only when SwiftUI can't do it (yet).

**MVVM**: Clean separation. Views stay dumb. ViewModels handle logic and are testable without UI. Apple's `@Observable` and `@StateObject` fit this pattern naturally.

**Feature-based structure**: Scales better than type-based (all views in one folder, all models in another). Each feature is self-contained and can be worked on independently.

**Protocols for dependencies**: Makes testing trivial. Swap real services for mocks. Also enables previews with mock data.

**Native over third-party**: Less maintenance burden. Apple frameworks are optimized and well-documented. Alamofire? URLSession is fine. Kingfisher? AsyncImage + custom cache. Only add dependencies when truly needed.

**Structured concurrency**: async/await and actors are safer than GCD. Compiler catches race conditions. Code reads linearly.

## Project Structure

```
App/
├── App.swift                 # @main entry point
├── Features/
│   ├── Auth/
│   │   ├── AuthView.swift
│   │   ├── AuthViewModel.swift
│   │   └── AuthService.swift
│   └── Home/
│       ├── HomeView.swift
│       └── HomeViewModel.swift
├── Core/
│   ├── Models/
│   ├── Services/
│   ├── Extensions/
│   └── Utilities/
├── UI/
│   ├── Components/
│   └── Modifiers/
└── Resources/
    ├── Assets.xcassets
    └── Localizable.xcstrings
```

Group by feature, not by type. Each feature folder is self-contained.

## Swift Style

Follow Swift API Design Guidelines: https://www.swift.org/documentation/api-design-guidelines/

```swift
// Naming: clarity at the point of use
func insert(_ element: Element, at index: Int)  // Good
func insert(_ element: Element, position: Int)  // Bad - "position" unclear

// Prefer methods over free functions
array.sort()           // Good
sort(&array)           // Avoid

// Use grammatical English
x.makeIterator()       // Good - noun phrase result
x.makeIterating()      // Bad

// Bool properties read as assertions
isEmpty                // Good
empty                  // Bad
```

### Optionals

```swift
// Prefer guard for early exit
guard let user = user else { return }

// Use optional chaining
let name = user?.profile?.displayName

// Avoid force unwrap except in tests
let value = dictionary["key"]!  // Only if guaranteed

// Prefer nil coalescing
let name = user?.name ?? "Anonymous"

// Use map/flatMap for transforms
let uppercased = optionalString.map { $0.uppercased() }
```

### Error Handling

```swift
// Define domain-specific errors
enum NetworkError: LocalizedError {
    case noConnection
    case timeout
    case serverError(statusCode: Int)

    var errorDescription: String? {
        switch self {
        case .noConnection: return "No internet connection"
        case .timeout: return "Request timed out"
        case .serverError(let code): return "Server error (\(code))"
        }
    }
}

// Use Result for async callbacks (pre-async/await)
// Prefer async/await for new code
func fetchUser() async throws -> User {
    // ...
}
```

## SwiftUI Patterns

### View Structure

```swift
struct ProfileView: View {
    @StateObject private var viewModel = ProfileViewModel()

    var body: some View {
        content
            .navigationTitle("Profile")
            .task { await viewModel.load() }
            .alert("Error", isPresented: $viewModel.showError) {
                Button("OK") { }
            } message: {
                Text(viewModel.errorMessage)
            }
    }

    // Extract complex views into computed properties
    private var content: some View {
        List {
            headerSection
            settingsSection
        }
    }

    private var headerSection: some View {
        Section {
            // ...
        }
    }
}
```

### ViewModel

```swift
@MainActor
final class ProfileViewModel: ObservableObject {
    @Published private(set) var user: User?
    @Published private(set) var isLoading = false
    @Published var showError = false
    @Published private(set) var errorMessage = ""

    private let userService: UserServiceProtocol

    init(userService: UserServiceProtocol = UserService.shared) {
        self.userService = userService
    }

    func load() async {
        isLoading = true
        defer { isLoading = false }

        do {
            user = try await userService.fetchCurrentUser()
        } catch {
            errorMessage = error.localizedDescription
            showError = true
        }
    }
}
```

### State Management

```swift
// Local state
@State private var isExpanded = false

// Observable object (owned by this view)
@StateObject private var viewModel = ViewModel()

// Observable object (passed from parent)
@ObservedObject var viewModel: ViewModel

// Environment values
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme

// App-wide state
@EnvironmentObject var appState: AppState
```

## Concurrency

Use Swift's structured concurrency.

```swift
// Async/await
func fetchData() async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

// Task groups for parallel work
let results = await withTaskGroup(of: Item?.self) { group in
    for id in ids {
        group.addTask { try? await fetchItem(id) }
    }
    return await group.compactMap { $0 }
}

// Actors for thread-safe state
actor ImageCache {
    private var cache: [URL: UIImage] = [:]

    func image(for url: URL) -> UIImage? {
        cache[url]
    }

    func store(_ image: UIImage, for url: URL) {
        cache[url] = image
    }
}

// MainActor for UI updates
@MainActor
func updateUI() {
    // Safe to update UI here
}
```

## Networking

```swift
protocol APIClientProtocol {
    func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
}

struct APIClient: APIClientProtocol {
    private let session: URLSession
    private let decoder: JSONDecoder

    func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
        let request = try endpoint.urlRequest()
        let (data, response) = try await session.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw NetworkError.invalidResponse
        }

        guard 200..<300 ~= httpResponse.statusCode else {
            throw NetworkError.serverError(statusCode: httpResponse.statusCode)
        }

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

## Testing

```swift
// Use protocols for dependency injection
final class ProfileViewModelTests: XCTestCase {
    private var sut: ProfileViewModel!
    private var mockUserService: MockUserService!

    override func setUp() {
        super.setUp()
        mockUserService = MockUserService()
        sut = ProfileViewModel(userService: mockUserService)
    }

    func test_load_success_updatesUser() async {
        // Given
        let expectedUser = User.mock()
        mockUserService.fetchResult = .success(expectedUser)

        // When
        await sut.load()

        // Then
        XCTAssertEqual(sut.user, expectedUser)
        XCTAssertFalse(sut.isLoading)
    }

    func test_load_failure_showsError() async {
        // Given
        mockUserService.fetchResult = .failure(NetworkError.noConnection)

        // When
        await sut.load()

        // Then
        XCTAssertTrue(sut.showError)
        XCTAssertNil(sut.user)
    }
}
```

## Common Commands

```bash
# Build
xcodebuild -scheme App -sdk iphonesimulator build

# Test
xcodebuild -scheme App -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15' test

# SwiftLint (if installed)
swiftlint lint --strict

# SwiftFormat (if installed)
swiftformat . --lint
```

## Things to Avoid

- Force unwrapping in production code
- Massive view controllers/views - break them up
- Singletons everywhere - use dependency injection
- Ignoring Apple's Human Interface Guidelines
- Third-party dependencies when Apple provides native solutions
- Stringly-typed APIs - use enums and strong types
- Blocking the main thread

## Resources

- Swift API Design Guidelines: https://www.swift.org/documentation/api-design-guidelines/
- Human Interface Guidelines: https://developer.apple.com/design/human-interface-guidelines/
- WWDC Videos: https://developer.apple.com/videos/

Comments

No comments yet

Be the first to share your thoughts!