Skip to main content
Glama
swift-development.mdc17.9 kB
--- 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] = [:] ```

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/madebyaris/rakitui-ai'

If you have feedback or need assistance with the MCP directory API, please join our Discord server