ios-clean-architecture/SKILL.md
Implement Clean Architecture patterns for iOS with proper layer separation, use cases, repositories, and dependency inversion. Use when designing app architecture, separating concerns, implementing use cases, creating repositories, or making code testable and maintainable. Triggers on Clean Architecture, use case, repository, domain layer, data layer, presentation layer, SOLID, Uncle Bob, dependency inversion, entities.
npx skillsauth add abanoub-ashraf/manus-skills-import ios-clean-architectureInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an expert in Clean Architecture for iOS. When this skill activates, help implement proper layer separation and dependency rules.
Source code dependencies must point only inward, toward higher-level policies.
┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ (Views, ViewModels, UI) │
└─────────────────────────────────────────────────────────────┘
│
▼ depends on
┌─────────────────────────────────────────────────────────────┐
│ Domain Layer │
│ (Use Cases, Entities, Interfaces) │
└─────────────────────────────────────────────────────────────┘
▲
│ implements
┌─────────────────────────────────────────────────────────────┐
│ Data Layer │
│ (Repositories, Data Sources, DTOs) │
└─────────────────────────────────────────────────────────────┘
| Layer | Contains | Depends On | |-------|----------|------------| | Presentation | Views, ViewModels, Coordinators | Domain | | Domain | Entities, Use Cases, Repository Protocols | Nothing | | Data | Repository Implementations, API, Database | Domain |
MyApp/
├── App/
│ ├── DI/
│ │ └── DependencyContainer.swift
│ ├── Coordinators/
│ │ └── AppCoordinator.swift
│ └── MyApp.swift
│
├── Presentation/
│ ├── Features/
│ │ ├── Home/
│ │ │ ├── HomeView.swift
│ │ │ └── HomeViewModel.swift
│ │ └── Profile/
│ │ ├── ProfileView.swift
│ │ └── ProfileViewModel.swift
│ └── Common/
│ └── Components/
│
├── Domain/
│ ├── Entities/
│ │ ├── User.swift
│ │ └── Product.swift
│ ├── UseCases/
│ │ ├── GetUserUseCase.swift
│ │ ├── GetProductsUseCase.swift
│ │ └── PurchaseProductUseCase.swift
│ ├── Repositories/
│ │ ├── UserRepositoryProtocol.swift
│ │ └── ProductRepositoryProtocol.swift
│ └── Errors/
│ └── DomainError.swift
│
└── Data/
├── Repositories/
│ ├── UserRepository.swift
│ └── ProductRepository.swift
├── DataSources/
│ ├── Remote/
│ │ ├── APIClient.swift
│ │ └── DTOs/
│ │ ├── UserDTO.swift
│ │ └── ProductDTO.swift
│ └── Local/
│ ├── Database.swift
│ └── UserDefaults/
└── Mappers/
├── UserMapper.swift
└── ProductMapper.swift
// Domain/Entities/User.swift
struct User: Equatable, Identifiable {
let id: String
let name: String
let email: String
let subscription: SubscriptionTier
enum SubscriptionTier {
case free
case premium
case enterprise
}
var canAccessPremiumFeatures: Bool {
subscription != .free
}
}
// Domain/Entities/Product.swift
struct Product: Equatable, Identifiable {
let id: String
let name: String
let price: Decimal
let category: Category
let stock: Int
var isAvailable: Bool {
stock > 0
}
enum Category: String, CaseIterable {
case electronics
case clothing
case food
}
}
// Domain/Repositories/UserRepositoryProtocol.swift
protocol UserRepositoryProtocol {
func getUser(id: String) async throws -> User
func getCurrentUser() async throws -> User
func updateUser(_ user: User) async throws -> User
func deleteUser(id: String) async throws
}
// Domain/Repositories/ProductRepositoryProtocol.swift
protocol ProductRepositoryProtocol {
func getProducts() async throws -> [Product]
func getProduct(id: String) async throws -> Product
func searchProducts(query: String) async throws -> [Product]
func getProductsByCategory(_ category: Product.Category) async throws -> [Product]
}
// Domain/UseCases/GetUserUseCase.swift
protocol GetUserUseCaseProtocol {
func execute(id: String) async throws -> User
}
final class GetUserUseCase: GetUserUseCaseProtocol {
private let userRepository: UserRepositoryProtocol
init(userRepository: UserRepositoryProtocol) {
self.userRepository = userRepository
}
func execute(id: String) async throws -> User {
guard !id.isEmpty else {
throw DomainError.invalidInput("User ID cannot be empty")
}
return try await userRepository.getUser(id: id)
}
}
// Domain/UseCases/PurchaseProductUseCase.swift
protocol PurchaseProductUseCaseProtocol {
func execute(productID: String, quantity: Int) async throws -> Order
}
final class PurchaseProductUseCase: PurchaseProductUseCaseProtocol {
private let productRepository: ProductRepositoryProtocol
private let orderRepository: OrderRepositoryProtocol
private let userRepository: UserRepositoryProtocol
init(
productRepository: ProductRepositoryProtocol,
orderRepository: OrderRepositoryProtocol,
userRepository: UserRepositoryProtocol
) {
self.productRepository = productRepository
self.orderRepository = orderRepository
self.userRepository = userRepository
}
func execute(productID: String, quantity: Int) async throws -> Order {
// Business rules live here
guard quantity > 0 else {
throw DomainError.invalidInput("Quantity must be positive")
}
let product = try await productRepository.getProduct(id: productID)
guard product.isAvailable else {
throw DomainError.productNotAvailable
}
guard product.stock >= quantity else {
throw DomainError.insufficientStock(available: product.stock)
}
let user = try await userRepository.getCurrentUser()
let order = Order(
id: UUID().uuidString,
userID: user.id,
productID: product.id,
quantity: quantity,
totalPrice: product.price * Decimal(quantity),
status: .pending
)
return try await orderRepository.createOrder(order)
}
}
// Domain/Errors/DomainError.swift
enum DomainError: LocalizedError {
case invalidInput(String)
case notFound(String)
case productNotAvailable
case insufficientStock(available: Int)
case unauthorized
case networkError
case unknown
var errorDescription: String? {
switch self {
case .invalidInput(let message):
return message
case .notFound(let entity):
return "\(entity) not found"
case .productNotAvailable:
return "This product is no longer available"
case .insufficientStock(let available):
return "Only \(available) items in stock"
case .unauthorized:
return "You don't have permission for this action"
case .networkError:
return "Network error. Please try again."
case .unknown:
return "An unexpected error occurred"
}
}
}
// Data/DataSources/Remote/DTOs/UserDTO.swift
struct UserDTO: Codable {
let id: String
let name: String
let email: String
let subscriptionType: String
let createdAt: String
let updatedAt: String
enum CodingKeys: String, CodingKey {
case id
case name
case email
case subscriptionType = "subscription_type"
case createdAt = "created_at"
case updatedAt = "updated_at"
}
}
// Data/Mappers/UserMapper.swift
enum UserMapper {
static func toDomain(_ dto: UserDTO) -> User {
User(
id: dto.id,
name: dto.name,
email: dto.email,
subscription: mapSubscription(dto.subscriptionType)
)
}
static func toDTO(_ entity: User) -> UserDTO {
UserDTO(
id: entity.id,
name: entity.name,
email: entity.email,
subscriptionType: entity.subscription.rawValue,
createdAt: "",
updatedAt: ""
)
}
private static func mapSubscription(_ type: String) -> User.SubscriptionTier {
switch type {
case "premium": return .premium
case "enterprise": return .enterprise
default: return .free
}
}
}
extension User.SubscriptionTier {
var rawValue: String {
switch self {
case .free: return "free"
case .premium: return "premium"
case .enterprise: return "enterprise"
}
}
}
// Data/Repositories/UserRepository.swift
final class UserRepository: UserRepositoryProtocol {
private let apiClient: APIClientProtocol
private let localStorage: UserLocalStorageProtocol
init(apiClient: APIClientProtocol, localStorage: UserLocalStorageProtocol) {
self.apiClient = apiClient
self.localStorage = localStorage
}
func getUser(id: String) async throws -> User {
// Try cache first
if let cached = try? await localStorage.getUser(id: id) {
return cached
}
// Fetch from API
let dto: UserDTO = try await apiClient.request(
endpoint: .user(id: id),
method: .get
)
let user = UserMapper.toDomain(dto)
// Cache for next time
try? await localStorage.saveUser(user)
return user
}
func getCurrentUser() async throws -> User {
let dto: UserDTO = try await apiClient.request(
endpoint: .currentUser,
method: .get
)
return UserMapper.toDomain(dto)
}
func updateUser(_ user: User) async throws -> User {
let dto = UserMapper.toDTO(user)
let responseDTO: UserDTO = try await apiClient.request(
endpoint: .user(id: user.id),
method: .put,
body: dto
)
let updatedUser = UserMapper.toDomain(responseDTO)
try? await localStorage.saveUser(updatedUser)
return updatedUser
}
func deleteUser(id: String) async throws {
try await apiClient.request(
endpoint: .user(id: id),
method: .delete
)
try? await localStorage.deleteUser(id: id)
}
}
// Presentation/Features/Profile/ProfileViewModel.swift
@Observable
@MainActor
final class ProfileViewModel {
// State
private(set) var user: User?
private(set) var isLoading = false
private(set) var error: DomainError?
// Use Cases
private let getUserUseCase: GetUserUseCaseProtocol
private let updateUserUseCase: UpdateUserUseCaseProtocol
init(
getUserUseCase: GetUserUseCaseProtocol,
updateUserUseCase: UpdateUserUseCaseProtocol
) {
self.getUserUseCase = getUserUseCase
self.updateUserUseCase = updateUserUseCase
}
func loadUser(id: String) async {
isLoading = true
error = nil
do {
user = try await getUserUseCase.execute(id: id)
} catch let domainError as DomainError {
error = domainError
} catch {
self.error = .unknown
}
isLoading = false
}
func updateName(_ name: String) async {
guard var currentUser = user else { return }
isLoading = true
currentUser = User(
id: currentUser.id,
name: name,
email: currentUser.email,
subscription: currentUser.subscription
)
do {
user = try await updateUserUseCase.execute(user: currentUser)
} catch let domainError as DomainError {
error = domainError
} catch {
self.error = .unknown
}
isLoading = false
}
}
// Presentation/Features/Profile/ProfileView.swift
struct ProfileView: View {
@State var viewModel: ProfileViewModel
let userID: String
var body: some View {
Group {
if viewModel.isLoading && viewModel.user == nil {
ProgressView()
} else if let user = viewModel.user {
ProfileContentView(user: user, onNameUpdate: updateName)
} else if let error = viewModel.error {
ErrorView(error: error, onRetry: loadUser)
}
}
.task {
await loadUser()
}
}
private func loadUser() async {
await viewModel.loadUser(id: userID)
}
private func updateName(_ name: String) {
Task {
await viewModel.updateName(name)
}
}
}
// App/DI/DependencyContainer.swift
@MainActor
final class DependencyContainer {
// MARK: - Data Layer
private lazy var apiClient: APIClientProtocol = APIClient()
private lazy var userLocalStorage: UserLocalStorageProtocol = UserLocalStorage()
// MARK: - Repositories
private lazy var userRepository: UserRepositoryProtocol = {
UserRepository(apiClient: apiClient, localStorage: userLocalStorage)
}()
private lazy var productRepository: ProductRepositoryProtocol = {
ProductRepository(apiClient: apiClient)
}()
// MARK: - Use Cases
func makeGetUserUseCase() -> GetUserUseCaseProtocol {
GetUserUseCase(userRepository: userRepository)
}
func makeUpdateUserUseCase() -> UpdateUserUseCaseProtocol {
UpdateUserUseCase(userRepository: userRepository)
}
func makePurchaseProductUseCase() -> PurchaseProductUseCaseProtocol {
PurchaseProductUseCase(
productRepository: productRepository,
orderRepository: makeOrderRepository(),
userRepository: userRepository
)
}
// MARK: - ViewModels
func makeProfileViewModel() -> ProfileViewModel {
ProfileViewModel(
getUserUseCase: makeGetUserUseCase(),
updateUserUseCase: makeUpdateUserUseCase()
)
}
}
final class GetUserUseCaseTests: XCTestCase {
var sut: GetUserUseCase!
var mockRepository: MockUserRepository!
override func setUp() {
mockRepository = MockUserRepository()
sut = GetUserUseCase(userRepository: mockRepository)
}
func test_execute_withValidID_returnsUser() async throws {
// Given
let expectedUser = User(id: "123", name: "John", email: "[email protected]", subscription: .free)
mockRepository.getUserResult = .success(expectedUser)
// When
let user = try await sut.execute(id: "123")
// Then
XCTAssertEqual(user, expectedUser)
XCTAssertEqual(mockRepository.getUserCallCount, 1)
}
func test_execute_withEmptyID_throwsError() async {
// When/Then
do {
_ = try await sut.execute(id: "")
XCTFail("Expected error")
} catch let error as DomainError {
XCTAssertEqual(error, .invalidInput("User ID cannot be empty"))
} catch {
XCTFail("Wrong error type")
}
}
}
@MainActor
final class ProfileViewModelTests: XCTestCase {
var sut: ProfileViewModel!
var mockGetUserUseCase: MockGetUserUseCase!
var mockUpdateUserUseCase: MockUpdateUserUseCase!
override func setUp() {
mockGetUserUseCase = MockGetUserUseCase()
mockUpdateUserUseCase = MockUpdateUserUseCase()
sut = ProfileViewModel(
getUserUseCase: mockGetUserUseCase,
updateUserUseCase: mockUpdateUserUseCase
)
}
func test_loadUser_success_updatesState() async {
// Given
let user = User.mock
mockGetUserUseCase.result = .success(user)
// When
await sut.loadUser(id: "123")
// Then
XCTAssertEqual(sut.user, user)
XCTAssertFalse(sut.isLoading)
XCTAssertNil(sut.error)
}
}
development
Design principles for building polished, native-feeling SwiftUI apps and widgets. Use this skill when creating or modifying SwiftUI views, iOS widgets (WidgetKit), or any native Apple UI. Ensures proper spacing, typography, colors, and widget implementations that look and feel like quality apps rather than AI-generated slop.
data-ai
Design and implement SwiftUI views, components, and app architecture. Use when creating new SwiftUI views, implementing MVVM/TCA patterns, managing state with @Observable, @State, @Binding, or @Environment, designing navigation flows, or structuring iOS app architecture. Triggers on SwiftUI, view model, state management, navigation, coordinator pattern.
development
Implement, review, or improve SwiftUI animations and transitions. Use when adding implicit or explicit animations with withAnimation, configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (bounce, pulse, variableColor, breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
testing
Audit SwiftUI views for accessibility (iOS + macOS) with patch-ready fixes