---
description: "Swift development: SwiftUI, Combine, async/await, iOS patterns, and Apple platform conventions"
globs: ["**/*.swift", "**/*.xcodeproj/**", "**/*.xcworkspace/**", "**/Package.swift"]
alwaysApply: false
---
# Swift Development Patterns
Modern Swift patterns for iOS, macOS, and Apple platform development.
## CRITICAL: Agentic-First Swift/iOS Development
### Pre-Development Verification (MANDATORY)
Before writing ANY Swift/iOS code:
```
1. CHECK XCODE/SWIFT AVAILABILITY
→ run_terminal_cmd("swift --version")
→ run_terminal_cmd("xcodebuild -version")
→ run_terminal_cmd("xcrun simctl list") # Available simulators
2. VERIFY CURRENT VERSIONS (use web_search)
→ web_search("Swift version December 2024")
→ web_search("iOS SDK version December 2024")
→ web_search("Xcode latest version 2024")
3. CHECK EXISTING PROJECT
→ Does *.xcodeproj exist? If yes, use Xcode to modify!
→ Read Package.swift if it's a Swift Package
→ NEVER manually edit project.pbxproj files!
```
### ⚠️ CRITICAL: Xcode Project Files
**NEVER manually create or edit:**
- `*.xcodeproj/project.pbxproj` - This is a complex binary-like file managed by Xcode
- `*.xcworkspace/contents.xcworkspacedata`
- `*.xcodeproj/xcuserdata/*`
**These files MUST be created through:**
- Xcode IDE (Create New Project)
- `swift package init` for Swift Package Manager projects
- `xcodebuild` commands for CI/CD
**Why?** The `.pbxproj` file has:
- UUID references that must be consistent
- Specific formatting Xcode expects
- Build settings that are complex to replicate
- Manual creation will result in CORRUPTED projects
### CLI-First Swift Development
**For Swift Packages (preferred for libraries):**
```bash
# Create new package (NEVER manually create Package.swift)
swift package init --type library
swift package init --type executable
swift package init --type tool
# Add dependencies
# Edit Package.swift, then:
swift package resolve
swift build
swift test
```
**For iOS/macOS Apps:**
```bash
# You MUST use Xcode to create .xcodeproj
# There is NO CLI equivalent for full iOS projects
# Build from command line (project must exist)
xcodebuild -project MyApp.xcodeproj -scheme MyApp -sdk iphonesimulator build
# Run tests
xcodebuild test -project MyApp.xcodeproj -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15'
```
### Post-Edit Verification
After ANY Swift code changes:
```bash
# Swift Package
swift build
swift test
# Xcode Project (must have valid .xcodeproj)
xcodebuild -project MyApp.xcodeproj -scheme MyApp -sdk iphonesimulator build
# SwiftLint (if available)
swiftlint lint
# Format
swift-format format --in-place --recursive Sources/
```
### Common Swift Syntax Traps (Avoid These!)
```swift
// WRONG: Force unwrapping optionals
let name = user.name! // Crashes if nil!
// CORRECT: Safe unwrapping
guard let name = user.name else {
return
}
// Or use optional chaining
let name = user.name ?? "Unknown"
// WRONG: Strong reference cycles
class Parent {
var child: Child?
}
class Child {
var parent: Parent? // Creates retain cycle!
}
// CORRECT: Use weak or unowned
class Child {
weak var parent: Parent?
}
// WRONG: Blocking main thread
func loadData() {
let data = URLSession.shared.data(from: url) // Blocks UI!
}
// CORRECT: Use async/await
func loadData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// WRONG: Not using @MainActor for UI updates
class ViewModel: ObservableObject {
@Published var items: [Item] = [] // May update from background!
}
// CORRECT: Use @MainActor
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = [] // Guaranteed main thread
}
```
---
## SwiftUI Fundamentals
### View Structure
```swift
struct ContentView: View {
// State at the top
@State private var isLoading = false
@State private var items: [Item] = []
// Environment and observed objects
@EnvironmentObject var appState: AppState
@ObservedObject var viewModel: ContentViewModel
// Computed properties
private var filteredItems: [Item] {
items.filter { $0.isActive }
}
var body: some View {
NavigationStack {
List(filteredItems) { item in
ItemRow(item: item)
}
.navigationTitle("Items")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Add", action: addItem)
}
}
}
.task {
await loadItems()
}
}
// Actions at the bottom
private func addItem() { ... }
private func loadItems() async { ... }
}
```
### Property Wrappers
```swift
// @State - owned by the view, simple value types
@State private var count = 0
@State private var text = ""
// @Binding - reference to parent's state
struct ChildView: View {
@Binding var isPresented: Bool
}
// @StateObject - owned by the view, reference types (create once)
@StateObject private var viewModel = ViewModel()
// @ObservedObject - reference from parent (don't create)
@ObservedObject var viewModel: ViewModel
// @EnvironmentObject - shared app-wide state
@EnvironmentObject var appState: AppState
// @Environment - system values
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
// @AppStorage - UserDefaults persistence
@AppStorage("username") var username = ""
```
### View Modifiers
```swift
struct ContentView: View {
var body: some View {
Text("Hello")
.font(.headline)
.foregroundColor(.primary)
.padding()
.background(.regularMaterial)
.cornerRadius(12)
.shadow(radius: 4)
}
}
// Custom modifier
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.background)
.cornerRadius(12)
.shadow(radius: 4)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardStyle())
}
}
```
---
## Async/Await
### Basic Async Functions
```swift
func fetchUser(id: String) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw APIError.invalidResponse
}
return try JSONDecoder().decode(User.self, from: data)
}
// Calling async functions
Task {
do {
let user = try await fetchUser(id: "123")
// Update UI on main actor
await MainActor.run {
self.user = user
}
} catch {
print("Error: \(error)")
}
}
```
### Concurrent Operations
```swift
// Parallel execution with async let
func fetchDashboard() async throws -> Dashboard {
async let user = fetchUser()
async let posts = fetchPosts()
async let notifications = fetchNotifications()
return try await Dashboard(
user: user,
posts: posts,
notifications: notifications
)
}
// TaskGroup for dynamic concurrency
func fetchAllUsers(ids: [String]) async throws -> [User] {
try await withThrowingTaskGroup(of: User.self) { group in
for id in ids {
group.addTask {
try await fetchUser(id: id)
}
}
var users: [User] = []
for try await user in group {
users.append(user)
}
return users
}
}
```
### MainActor
```swift
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
func loadItems() async {
isLoading = true
defer { isLoading = false }
do {
items = try await api.fetchItems()
} catch {
// Handle error
}
}
}
```
---
## Combine Framework
### Publishers and Subscribers
```swift
import Combine
class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var results: [SearchResult] = []
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.filter { !$0.isEmpty }
.flatMap { query in
self.search(query: query)
.catch { _ in Just([]) }
}
.receive(on: DispatchQueue.main)
.assign(to: &$results)
}
private func search(query: String) -> AnyPublisher<[SearchResult], Error> {
// Return search publisher
}
}
```
### Common Operators
```swift
// Transform
publisher.map { $0.name }
publisher.compactMap { Int($0) }
publisher.flatMap { fetchDetails(for: $0) }
// Filter
publisher.filter { $0.isValid }
publisher.removeDuplicates()
publisher.first()
// Combine
Publishers.CombineLatest(pub1, pub2)
Publishers.Merge(pub1, pub2)
Publishers.Zip(pub1, pub2)
// Error handling
publisher.catch { error in Just(defaultValue) }
publisher.retry(3)
publisher.replaceError(with: defaultValue)
// Timing
publisher.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
publisher.throttle(for: .seconds(1), scheduler: RunLoop.main, latest: true)
```
---
## MVVM Architecture
### ViewModel Pattern
```swift
@MainActor
protocol ItemsViewModelProtocol: ObservableObject {
var items: [Item] { get }
var isLoading: Bool { get }
var error: Error? { get }
func loadItems() async
func deleteItem(_ item: Item) async
}
@MainActor
final class ItemsViewModel: ItemsViewModelProtocol {
@Published private(set) var items: [Item] = []
@Published private(set) var isLoading = false
@Published private(set) var error: Error?
private let repository: ItemRepository
init(repository: ItemRepository) {
self.repository = repository
}
func loadItems() async {
isLoading = true
error = nil
do {
items = try await repository.fetchAll()
} catch {
self.error = error
}
isLoading = false
}
func deleteItem(_ item: Item) async {
do {
try await repository.delete(item)
items.removeAll { $0.id == item.id }
} catch {
self.error = error
}
}
}
```
### View with ViewModel
```swift
struct ItemsView: View {
@StateObject private var viewModel: ItemsViewModel
init(repository: ItemRepository) {
_viewModel = StateObject(wrappedValue: ItemsViewModel(repository: repository))
}
var body: some View {
Group {
if viewModel.isLoading {
ProgressView()
} else if let error = viewModel.error {
ErrorView(error: error, retry: { Task { await viewModel.loadItems() } })
} else {
List(viewModel.items) { item in
ItemRow(item: item)
}
}
}
.task {
await viewModel.loadItems()
}
}
}
```
---
## Error Handling
### Custom Errors
```swift
enum APIError: LocalizedError {
case invalidURL
case invalidResponse
case networkError(underlying: Error)
case decodingError(underlying: Error)
case notFound
case unauthorized
var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid URL"
case .invalidResponse:
return "Invalid server response"
case .networkError(let error):
return "Network error: \(error.localizedDescription)"
case .decodingError(let error):
return "Failed to decode response: \(error.localizedDescription)"
case .notFound:
return "Resource not found"
case .unauthorized:
return "Unauthorized access"
}
}
}
```
### Result Type
```swift
func fetchData() -> Result<Data, APIError> {
// Synchronous result
}
// Usage
switch fetchData() {
case .success(let data):
process(data)
case .failure(let error):
handleError(error)
}
// Map and flatMap
let result = fetchData()
.map { String(data: $0, encoding: .utf8) }
.flatMap { string in
guard let value = string else {
return .failure(.decodingError(underlying: DecodingError.dataCorrupted(...)))
}
return .success(value)
}
```
---
## Data Persistence
### SwiftData (iOS 17+)
```swift
import SwiftData
@Model
final class Item {
var name: String
var timestamp: Date
var isCompleted: Bool
@Relationship(deleteRule: .cascade)
var tags: [Tag]
init(name: String) {
self.name = name
self.timestamp = .now
self.isCompleted = false
self.tags = []
}
}
// In App
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Item.self, Tag.self])
}
}
// In View
struct ItemsView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \Item.timestamp, order: .reverse) private var items: [Item]
var body: some View {
List(items) { item in
Text(item.name)
}
}
func addItem() {
let item = Item(name: "New Item")
modelContext.insert(item)
}
}
```
### UserDefaults with Property Wrapper
```swift
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
// Usage
enum Settings {
@UserDefault(key: "hasSeenOnboarding", defaultValue: false)
static var hasSeenOnboarding: Bool
@UserDefault(key: "selectedTheme", defaultValue: "system")
static var selectedTheme: String
}
```
---
## Networking
### Modern URLSession
```swift
actor APIClient {
private let session: URLSession
private let decoder: JSONDecoder
private let baseURL: URL
init(baseURL: URL) {
self.baseURL = baseURL
self.session = URLSession.shared
self.decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
}
func fetch<T: Decodable>(_ endpoint: String) async throws -> T {
let url = baseURL.appendingPathComponent(endpoint)
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
switch httpResponse.statusCode {
case 200...299:
return try decoder.decode(T.self, from: data)
case 401:
throw APIError.unauthorized
case 404:
throw APIError.notFound
default:
throw APIError.invalidResponse
}
}
}
```
---
## Testing
### Unit Tests
```swift
import XCTest
@testable import MyApp
final class ItemViewModelTests: XCTestCase {
var sut: ItemsViewModel!
var mockRepository: MockItemRepository!
@MainActor
override func setUp() {
super.setUp()
mockRepository = MockItemRepository()
sut = ItemsViewModel(repository: mockRepository)
}
@MainActor
func testLoadItemsSuccess() async {
// Given
let expectedItems = [Item(name: "Test")]
mockRepository.itemsToReturn = expectedItems
// When
await sut.loadItems()
// Then
XCTAssertEqual(sut.items, expectedItems)
XCTAssertFalse(sut.isLoading)
XCTAssertNil(sut.error)
}
@MainActor
func testLoadItemsFailure() async {
// Given
mockRepository.errorToThrow = APIError.networkError(underlying: URLError(.notConnectedToInternet))
// When
await sut.loadItems()
// Then
XCTAssertTrue(sut.items.isEmpty)
XCTAssertNotNil(sut.error)
}
}
```
### UI Tests
```swift
import XCTest
final class MyAppUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["--uitesting"]
app.launch()
}
func testAddItem() {
// Tap add button
app.navigationBars.buttons["Add"].tap()
// Fill form
let nameField = app.textFields["itemName"]
nameField.tap()
nameField.typeText("New Item")
// Save
app.buttons["Save"].tap()
// Verify
XCTAssertTrue(app.staticTexts["New Item"].exists)
}
}
```
---
## Swift Conventions
### Naming
```swift
// Types - PascalCase
struct UserProfile { }
class NetworkManager { }
enum LoadingState { }
protocol DataProvider { }
// Properties & methods - camelCase
let userName: String
func fetchUserData() async { }
// Boolean properties - use is/has/can prefix
var isLoading: Bool
var hasPermission: Bool
var canEdit: Bool
// Protocols - use -able, -ible, or describe capability
protocol Identifiable { }
protocol DataProviding { }
```
### Access Control
```swift
// public - accessible from other modules
public struct APIResponse { }
// internal (default) - accessible within module
struct ViewModel { }
// fileprivate - accessible within file
fileprivate func helper() { }
// private - accessible within enclosing declaration
private var cache: [String: Data] = [:]
```