--- description: 'Best practices and patterns for Swift' applyTo: "**/*.swift, **/Package.swift, **/Package.resolved" --- # Swift Development Instructions ## Core Directives You WILL follow Toyota OneApp iOS development standards and architectural patterns when working with Swift code. You WILL prioritize modern Swift practices including async/await, SwiftUI, and local Swift Package Manager (SPM) modules. You WILL adhere to Clean Architecture principles as defined by Robert C. Martin. You WILL NEVER introduce RxSwift or Combine for new code - use async/await and Swift concurrency instead. ### Dependency Management Policy CRITICAL: This project is migrating away from CocoaPods to Swift Package Manager (SPM). You MUST NOT suggest or add any new CocoaPods dependencies. You WILL use Swift Package Manager for all dependency management. You WILL use local Swift packages in `localPackages/` for internal modules. ## Project Structure ### Local Package Organization You MUST organize code into local Swift packages within the `localPackages/` directory. Each feature MUST be implemented as a separate Swift package following this naming convention: `{FeatureName}Feature` **Package Structure Example:** ``` localPackages/ClimateFeature/ ├── Package.swift ├── Sources/ │ └── ClimateFeature/ │ ├── Climate/ │ │ ├── Domain/ # Business logic, entities, repository protocols │ │ ├── Presentation/ # Views, StateNotifiers, UI components │ │ ├── Application/ # Use cases, business workflows │ │ └── Mocks/ # Mock implementations for testing and previews │ └── ClimateSchedule/ │ ├── Domain/ │ ├── Presentation/ │ ├── Application/ │ ├── DataAccess/ # Repository implementations, API clients │ └── Mocks/ └── Tests/ └── ClimateFeatureTests/ ``` ### Package Dependencies You MUST declare dependencies in `Package.swift` following these patterns: - Local package dependencies use `.package(path: "../{PackageName}")` - External dependencies use `.package(url:...)` with version constraints - Set minimum platform to `.iOS(.v17)` for new packages **Example Package.swift:** ```swift // swift-tools-version: 5.9 import PackageDescription public let package = Package( name: "ClimateFeature", platforms: [ .iOS(.v17) ], products: [ .library(name: "ClimateFeature", targets: ["ClimateFeature"]), ], dependencies: [ .package(path: "../Components"), .package(path: "../Navigation"), .package(path: "../Analytics"), .package(path: "../NetworkClients"), ], targets: [ .target( name: "ClimateFeature", dependencies: [ "Components", "Navigation", "Analytics", "NetworkClients" ] ), .testTarget( name: "ClimateFeatureTests", dependencies: ["ClimateFeature"] ), ] ) ``` ## GraphQL Networking Layer ### Overview The Toyota OneApp iOS project uses **Apollo iOS (v1.7.0)** for GraphQL networking, organized in local Swift packages: - `localPackages/GraphQLLib` - Core GraphQL networking library with Apollo client wrappers - `localPackages/NetworkClients` - API client implementations, GraphQL operations, and generated schema ### GraphQL Operations **Location:** `localPackages/NetworkClients/Sources/NetworkClients/GraphQL/` You WILL organize GraphQL operations by feature domain: - **Queries:** `Operations/Query.graphql` - **Mutations:** `Operations/Mutation.graphql` - **Subscriptions:** `Operations/Subscription.graphql` - **Feature-specific:** `Operations/{FeatureName}/{Operation}.graphql` **Schema Location:** `GraphQL/schema.graphqls` (auto-generated from introspection) ### Apollo Codegen You WILL regenerate GraphQL types after modifying operations: ```bash cd localPackages/NetworkClients/Sources/NetworkClients/GraphQL ./Apollo-codegen/apollo-ios-cli generate ``` **Configuration:** `apollo-codegen-config.json` - Schema namespace: `VehicleStateAPI` - Generated types: `GraphQL/VehicleStateAPI/` ### Network Client Architecture **Entry Point:** ```swift // Access GraphQL client let client = NetworkClients.graphQLApi() ``` **Client Stack:** ``` NetworkClients.graphQLApi() └── GraphQLApiClient ├── AuthenticationService (token refresh) ├── RestDefaultHeaderService (HTTP headers) └── GraphService (from GraphQLLib) └── GraphClient (Apollo HTTP + WebSocket) ``` ### Making GraphQL Requests You WILL use async/await patterns with Apollo GraphQL: ```swift // Example: Execute a query let client = NetworkClients.graphQLApi() let result = await client.authenticated.call( operation: GetVehicleStatusQuery(vin: vehicleVin), additionalHeaders: [:] ) switch result { case .success(let response): let vehicleStatus = response.data?.getVehicleStatus case .error(let message): // Handle error } ``` ### Authentication & Headers **Authentication:** - Token management: `AuthenticationService.swift` - Automatic refresh on 401/403 via `GraphAuthenticateInterceptor` - Retry policy configured per client (default: 1 retry for auth errors) **Standard Headers:** ```swift [ "x-channel": "oneapp", "x-os-name": systemName, "x-os-version": systemVersion, "x-app-version": appVersion, "x-app-brand": appBrand, "x-locale": language, "x-api-key": apiKey, "x-guid": guid, "x-device-id": deviceId, "x-correlation-id": correlationId ] ``` ### Error Handling You WILL handle GraphQL errors using the established error types: ```swift // GraphQL-specific errors enum GraphQLLibError: Error { case queryDocumentError case invalidJsonError case invalidToken case graphClientError(Error) } // Network errors extension Error { var isNetworkError: Bool { /* ... */ } var isNetworkTimedout: Bool { /* ... */ } } ``` **Retry Configuration:** - 401/403: 1 retry with token refresh (1 second delay) - 5xx errors: 3 retries, no delay - Exponential backoff via `GraphRetryInterceptor` ### Interceptor Chain You WILL understand the request/response flow: 1. `GraphDefaultHeaderInterceptor` → Adds standard headers 2. `GraphAuthenticateInterceptor` → Adds bearer token 3. `GraphCacheReadInterceptor` → Checks in-memory cache 4. `GraphRequestLogInterceptor` → Logs request 5. **HTTP/WebSocket Request** 6. `GraphResponseErrorInterceptor` → Parses errors 7. `GraphResponseLogInterceptor` → Logs response 8. `GraphCacheWriteInterceptor` → Updates cache ### Caching Strategy **Current Implementation:** - In-memory cache only (`GraphInMemoryNormalizedCache`) - Default policy: `.fetchIgnoringCacheCompletely` - NO persistent disk caching - Cache is cleared on app restart You WILL NOT implement persistent GraphQL caching unless explicitly requested. ### WebSocket Subscriptions You WILL use subscriptions for real-time updates: ```swift // Example: Subscribe to remote commands let subscription = SubscriptionRemoteCommandsSubscription(vin: vehicleVin) let result = await client.authenticated.call( operation: subscription, additionalHeaders: [:] ) ``` **WebSocket Transport:** - Auto-reconnection on auth refresh - Connection managed by `WebSocketTransportFactory` ### Key File Locations | Component | Path | |-----------|------| | **GraphQL Client** | `NetworkClients/Clients/GraphQLApiClient/Client/GraphQLApiClient.swift` | | **Authentication** | `NetworkClients/Clients/GraphQLApiClient/Client/AuthenticationService.swift` | | **Apollo Service** | `GraphQLLib/Networking/BaseNetwork/GraphService/GraphService.swift` | | **Operations** | `NetworkClients/GraphQL/Operations/*.graphql` | | **Schema** | `NetworkClients/GraphQL/schema.graphqls` | | **Generated Types** | `NetworkClients/GraphQL/VehicleStateAPI/` | | **Codegen Config** | `NetworkClients/GraphQL/apollo-codegen-config.json` | | **Interceptors** | `GraphQLLib/Networking/BaseNetwork/Interceptor/Graph/` | ## Clean Architecture Requirements ### Layer Responsibilities You MUST organize code into these layers within each feature: **Domain Layer** (`Domain/`) - You WILL define business entities, value objects, and domain models - You WILL create repository protocols (interfaces) - You WILL keep domain logic independent of frameworks and UI - You WILL use `internal` access control by default for domain types - CRITICAL: Domain layer MUST NOT depend on Presentation or DataAccess layers **Application Layer** (`Application/`) - You WILL implement use case protocols and concrete implementations - You WILL orchestrate business workflows and coordinate between repositories - You WILL handle business rule validation and orchestration - You MUST use async/await for asynchronous operations - CRITICAL: Use cases MUST be protocol-based for testability **Presentation Layer** (`Presentation/`) - You WILL create SwiftUI views and state notifiers - You WILL implement state management using `@Published` in state notifier classes - You WILL inject use cases via initializers for dependency injection - You WILL keep views declarative and presentation logic minimal - MANDATORY: You MUST create SwiftUI previews using `#Preview` macro for all views - CRITICAL: Views MUST NOT directly access repositories or data sources **DataAccess Layer** (`DataAccess/`) - You WILL implement repository protocols defined in Domain layer - You WILL handle network requests, database operations, and caching - You WILL use dependency injection for API clients and data sources - You MUST use async/await for all asynchronous data operations **Example Clean Architecture Implementation:** ```swift // Domain/Repos/ClimateScheduleRepo.swift internal protocol ClimateScheduleRepo { func fetchClimateScheduleList( generation: Generation, vin: String, make: VehicleMake ) async -> Result } // Application/ClimateScheduleUseCases.swift public protocol ClimateScheduleUseCases { var state: Published.Publisher { get } func toggleSchedule(id: Int) func refreshList(refresh: Bool) } // DataAccess/ClimateScheduleAPIRepo.swift internal final class ClimateScheduleAPIRepo: ClimateScheduleRepo { private let apiClient: APIClient init(apiClient: APIClient) { self.apiClient = apiClient } func fetchClimateScheduleList( generation: Generation, vin: String, make: VehicleMake ) async -> Result { // Implementation using async/await } } // Presentation/ClimateScheduleView.swift public struct ClimateScheduleView: View { @StateObject private var stateNotifier: ClimateScheduleStateNotifier public init(useCases: ClimateScheduleUseCases) { _stateNotifier = StateObject( wrappedValue: ClimateScheduleStateNotifier(useCases: useCases) ) } public var body: some View { List(stateNotifier.schedules) { schedule in Text(schedule.name) } } } #Preview { ClimateScheduleView(useCases: ClimateScheduleUseCasesMock()) } ``` ## Modern Swift Practices ### Async/Await Requirements You MUST use async/await for all asynchronous operations. You WILL NEVER use RxSwift or Combine in new code. You WILL migrate existing Combine/RxSwift code to async/await when making significant changes. **Async/Await Patterns:** ```swift // ✅ CORRECT: Use async/await for asynchronous functions func fetchClimateStatus(vehicle: Vehicle) async -> Result { do { let status = try await apiClient.fetchStatus(vehicle) return .success(status.isEnabled) } catch { return .failure(.networkError(error)) } } // ✅ CORRECT: Use Task for calling async from sync context func refreshData() { Task { await fetchScheduleList() } } // ❌ AVOID: Do not use Combine publishers in new code // var cancellables = Set() // apiClient.fetchStatus().sink { ... } // ❌ AVOID: Do not use RxSwift observables // apiClient.fetchStatus().subscribe(onNext: { ... }) ``` ### SwiftUI Requirements You MUST use SwiftUI for all new UI features. You WILL create declarative, composable views. You WILL use `@StateObject`, `@ObservedObject`, and `@Published` for state management. MANDATORY: You MUST provide `#Preview` for every SwiftUI view using mock implementations. **SwiftUI Patterns:** ```swift // ✅ CORRECT: SwiftUI view with proper state management and preview public struct ClimateDetailView: View { @StateObject private var stateNotifier: ClimateDetailStateNotifier @State private var showTimeSheet = false public init(useCases: ClimateDetailUseCases) { _stateNotifier = StateObject( wrappedValue: ClimateDetailStateNotifier(useCases: useCases) ) } public var body: some View { VStack { Text(stateNotifier.temperature) Button("Change Time") { showTimeSheet = true } } .sheet(isPresented: $showTimeSheet) { TimeSelectionView() } } } #Preview { ClimateDetailView(useCases: ClimateDetailUseCasesMock()) } // ✅ CORRECT: StateNotifier with async operations @MainActor final class ClimateDetailStateNotifier: ObservableObject { @Published var temperature: String = "" @Published var isLoading: Bool = false private let useCases: ClimateDetailUseCases init(useCases: ClimateDetailUseCases) { self.useCases = useCases } func updateTemperature(_ temp: Double) { Task { isLoading = true await useCases.changeTemperature(temp) isLoading = false } } } ``` ## Code Style and Quality ### SwiftLint and swift-format Compliance You MUST follow SwiftLint and swift-format configurations defined in `.swiftlint.yml` and `.swift-format`. **Key Requirements:** - You WILL use 4-space indentation - You WILL limit line length to 120 characters, but try to keep all expressions to 1 line when possible - You WILL include trailing commas in multiline collections (swift-format handles this) - You WILL use `private` access level for file-scoped declarations - You WILL avoid force unwrapping (`!`) and force try (`try!`) except in tests ### Naming Conventions You MUST follow Swift API Design Guidelines: - Types: `UpperCamelCase` (e.g., `ClimateScheduleRepo`, `Vehicle`) - Variables/Functions: `lowerCamelCase` (e.g., `fetchClimateStatus`, `reservationId`) - Constants: `lowerCamelCase` (e.g., `maxTemperature`, `defaultTimeout`) - Protocols: Descriptive names ending in `-able`, `-ing`, or role-based (e.g., `ClimateScheduleRepo`, `Codable`) ### File Headers You MUST include copyright headers in all Swift files. You WILL use the current year (2026) in copyright headers. ```swift // Copyright © 2026 Toyota. All rights reserved. ``` ### Code Organization You WILL organize code with MARK comments for major sections only. You WILL use MARK comments to separate significant logical groupings within a file. **MARK Comment Guidelines:** ```swift // ✅ CORRECT: MARK for major sections and protocols // MARK: - Climate Schedule Use Cases public protocol ClimateScheduleUseCases { var state: Published.Publisher { get } func toggleSchedule(id: Int) func refreshList(refresh: Bool) } final class ClimateScheduleLogic: ClimateScheduleUseCases { private let repository: ClimateScheduleRepo @Published private var _state = ClimateScheduleState() var state: Published.Publisher { $_state } init(repository: ClimateScheduleRepo) { self.repository = repository } func toggleSchedule(id: Int) { // Implementation } func refreshList(refresh: Bool) { // Implementation } } // ❌ AVOID: Excessive MARK comments for every section final class ExampleClass { // MARK: Properties // Too granular private let value: String // MARK: Initialization // Too granular init(value: String) { self.value = value } // MARK: Public Methods // Too granular func doSomething() {} } ``` ## Testing Requirements ### Unit Testing Standards You MUST write unit tests for all business logic, use cases, and repositories. You WILL create mock implementations in `Mocks/` subdirectory within each feature module. You WILL use XCTest framework for all tests. CRITICAL: Mocks MUST be reusable for both unit tests AND SwiftUI previews. ### Mock Organization You WILL place mock implementations in a `Mocks/` directory at the feature level: - Structure: `Sources/{FeatureName}/{SubFeature}/Mocks/` - Mocks are accessible to both production code (for previews) and test code - Mock classes MUST have public initializers for use in previews **Testing and Mock Patterns:** ```swift // Sources/ClimateFeature/ClimateSchedule/Mocks/ClimateScheduleRepoMock.swift public final class ClimateScheduleRepoMock: ClimateScheduleRepo { public var fetchClimateScheduleListResult: Result? public var fetchClimateScheduleListCallCount = 0 public init() {} public func fetchClimateScheduleList( generation: Generation, vin: String, make: VehicleMake ) async -> Result { fetchClimateScheduleListCallCount += 1 return fetchClimateScheduleListResult ?? .failure(.unknown) } } // Sources/ClimateFeature/ClimateSchedule/Mocks/ClimateScheduleUseCasesMock.swift public final class ClimateScheduleUseCasesMock: ClimateScheduleUseCases { public var state: Published.Publisher { $_state } @Published public var _state = ClimateScheduleState() public var toggleScheduleCallCount = 0 public init() {} public func toggleSchedule(id: Int) { toggleScheduleCallCount += 1 } public func refreshList(refresh: Bool) { // Mock implementation } } // Tests/ClimateFeatureTests/ClimateScheduleLogicTests.swift import XCTest @testable import ClimateFeature final class ClimateScheduleLogicTests: XCTestCase { private var sut: ClimateScheduleLogic! private var mockRepo: ClimateScheduleRepoMock! override func setUp() { super.setUp() mockRepo = ClimateScheduleRepoMock() sut = ClimateScheduleLogic(repository: mockRepo) } override func tearDown() { sut = nil mockRepo = nil super.tearDown() } func testFetchScheduleList_WhenSuccessful_UpdatesState() async { // Given let expectedData = ClimateScheduleSettingsData(schedules: []) mockRepo.fetchClimateScheduleListResult = .success(expectedData) // When await sut.fetchScheduleList() // Then XCTAssertEqual(mockRepo.fetchClimateScheduleListCallCount, 1) } } // Sources/ClimateFeature/ClimateSchedule/Presentation/ClimateScheduleView.swift public struct ClimateScheduleView: View { @StateObject private var stateNotifier: ClimateScheduleStateNotifier public init(useCases: ClimateScheduleUseCases) { _stateNotifier = StateObject( wrappedValue: ClimateScheduleStateNotifier(useCases: useCases) ) } public var body: some View { List { Text("Climate Schedules") } } } #Preview { ClimateScheduleView(useCases: ClimateScheduleUseCasesMock()) } ``` ## Fastlane Integration ### Fastlane Usage You WILL use Fastlane for build automation, testing, and deployment tasks. You MUST reference existing lanes defined in `fastlane/Fastfile` and imported Fastfiles. **Common Fastlane Commands:** - Build: `fastlane build` - Run tests: `fastlane test` - Lint code: `fastlane lint` - Run locally: `fastlane run_local` ### CI/CD Considerations You WILL ensure all code changes pass CI/CD pipelines: - SwiftLint must pass without warnings - All unit tests must pass - Build must succeed for all variants (Toyota/Lexus NA, Subaru, Toyota/Lexus AU) ## Migration Guidelines ### Legacy Code Interaction When working with existing legacy code: - You WILL gradually migrate from RxSwift/Combine to async/await when touching legacy modules - You WILL bridge UIKit and SwiftUI using `UIViewRepresentable` or `UIHostingController` when necessary - You WILL prioritize refactoring legacy code into Clean Architecture packages when feasible - You WILL NOT introduce new RxSwift/Combine dependencies ### Deprecation Patterns You WILL mark deprecated code with `@available` attributes: ```swift @available(*, deprecated, message: "Use async/await version instead") func fetchDataWithCombine() -> AnyPublisher { // Legacy implementation } // New async/await version func fetchData() async throws -> Data { // Modern implementation } ``` ## Error Handling ### Result Type Usage You WILL use Swift's `Result` type for operations that can fail: ```swift func fetchClimateStatus(vehicle: Vehicle) async -> Result { do { let status = try await apiClient.fetchStatus(vehicle) return .success(status.isEnabled) } catch let error as NetworkError { return .failure(.networkError(error)) } catch { return .failure(.unknown) } } ``` ### Error Types You WILL define custom error types conforming to `Error` protocol: ```swift enum ClimateScheduleError: Error { case invalidScheduleTime case scheduleConflict case networkFailure(underlying: Error) var localizedDescription: String { switch self { case .invalidScheduleTime: return "The schedule time is invalid" case .scheduleConflict: return "This schedule conflicts with an existing one" case .networkFailure(let error): return "Network error: \(error.localizedDescription)" } } } ``` ## Quick Reference ### How to compile: Here are examples on how to compile individual packages and the entire workspace: Individual Packages $ xcodebuild -workspace OneApp.xcworkspace -scheme {PACKAGE} -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17 Pro' clean build Entire workspace: $ xcodebuild -workspace OneApp.xcworkspace -scheme ToyotaOneApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build ### Do's and Don'ts **✅ DO:** - Use async/await for asynchronous operations - Create local Swift packages for new features - Follow Clean Architecture with Domain/Application/Presentation/DataAccess layers - Use SwiftUI for new UI features with StateNotifiers - Create `#Preview` for every SwiftUI view - Write unit tests with mocks stored in `Mocks/` subdirectory - Make mocks reusable for tests and previews - Include copyright headers - Follow SwiftLint and swift-format rules - Use protocol-based dependency injection - Comment the "why", never the "what" **❌ DON'T:** - Introduce RxSwift or Combine in new code - Use force unwrapping or force try in production code - Create direct dependencies between Presentation and DataAccess layers - Exceed 120 character line length - Skip writing unit tests for business logic - Skip creating previews for SwiftUI views - Use UIKit for new features (unless bridging is necessary) - Hardcode API endpoints or configuration values - Call state management classes "ViewModels" - use "StateNotifier" instead - Write comments that restate what code does - Create separate mock packages - keep mocks within the feature package - Suggest or add CocoaPods dependencies (project is migrating to SPM)