2
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!