Master iOS/macOS app architecture - MVVM, Clean Architecture, Coordinator, DI, Repository
Provides iOS/macOS architecture guidance for MVVM, Clean Architecture, and Coordinator patterns. Use when building scalable apps or refactoring to improve testability and separation of concerns.
/plugin marketplace add pluginagentmarketplace/custom-plugin-swift/plugin install swift-assistant@pluginagentmarketplace-swiftThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/ViewModel.swift.templateassets/config.yamlassets/schema.jsonreferences/GUIDE.mdreferences/PATTERNS.mdscripts/validate.pyDesign patterns and architectural approaches for scalable, testable Swift applications.
parameters:
architecture_pattern:
type: string
enum: [mvvm, mvc, tca, viper, clean]
default: mvvm
navigation_pattern:
type: string
enum: [coordinator, router, navigation_stack]
default: coordinator
di_approach:
type: string
enum: [manual, container, property_wrapper]
default: manual
| Pattern | Complexity | Testability | Best For |
|---|---|---|---|
| MVC | Low | Low | Simple apps |
| MVVM | Medium | High | Most apps |
| Clean | High | Very High | Large teams |
| TCA | High | Very High | Complex state |
| VIPER | Very High | Very High | Enterprise |
| Principle | Description |
|---|---|
| Separation of Concerns | Each layer has one job |
| Dependency Inversion | Depend on abstractions |
| Single Source of Truth | One place for state |
| Unidirectional Data Flow | State → View → Action → State |
| Layer | Responsibility |
|---|---|
| View | UI rendering only |
| ViewModel | Presentation logic |
| UseCase | Business logic |
| Repository | Data access |
| Service | External integrations |
// MARK: - Coordinator Protocol
protocol Coordinator: AnyObject {
var navigationController: UINavigationController { get }
var childCoordinators: [Coordinator] { get set }
func start()
}
extension Coordinator {
func addChild(_ coordinator: Coordinator) {
childCoordinators.append(coordinator)
}
func removeChild(_ coordinator: Coordinator) {
childCoordinators.removeAll { $0 === coordinator }
}
}
// MARK: - App Coordinator
final class AppCoordinator: Coordinator {
let navigationController: UINavigationController
var childCoordinators: [Coordinator] = []
private let dependencies: AppDependencies
init(navigationController: UINavigationController, dependencies: AppDependencies) {
self.navigationController = navigationController
self.dependencies = dependencies
}
func start() {
if dependencies.authService.isLoggedIn {
showMain()
} else {
showLogin()
}
}
private func showLogin() {
let coordinator = LoginCoordinator(
navigationController: navigationController,
dependencies: dependencies
)
coordinator.delegate = self
addChild(coordinator)
coordinator.start()
}
private func showMain() {
let coordinator = MainCoordinator(
navigationController: navigationController,
dependencies: dependencies
)
addChild(coordinator)
coordinator.start()
}
}
extension AppCoordinator: LoginCoordinatorDelegate {
func loginDidComplete(_ coordinator: LoginCoordinator) {
removeChild(coordinator)
showMain()
}
}
// MARK: - ViewModel
@MainActor
protocol ProductListViewModelProtocol: ObservableObject {
var products: [Product] { get }
var isLoading: Bool { get }
var error: Error? { get }
func loadProducts() async
func selectProduct(_ product: Product)
}
@MainActor
final class ProductListViewModel: ProductListViewModelProtocol {
@Published private(set) var products: [Product] = []
@Published private(set) var isLoading = false
@Published private(set) var error: Error?
private let getProductsUseCase: GetProductsUseCaseProtocol
private weak var coordinator: ProductCoordinator?
init(getProductsUseCase: GetProductsUseCaseProtocol, coordinator: ProductCoordinator) {
self.getProductsUseCase = getProductsUseCase
self.coordinator = coordinator
}
func loadProducts() async {
isLoading = true
error = nil
do {
products = try await getProductsUseCase.execute()
} catch {
self.error = error
}
isLoading = false
}
func selectProduct(_ product: Product) {
coordinator?.showProductDetail(product)
}
}
// MARK: - Domain Layer (Use Cases)
protocol GetProductsUseCaseProtocol {
func execute() async throws -> [Product]
}
final class GetProductsUseCase: GetProductsUseCaseProtocol {
private let repository: ProductRepositoryProtocol
init(repository: ProductRepositoryProtocol) {
self.repository = repository
}
func execute() async throws -> [Product] {
let products = try await repository.getProducts()
// Business logic: filter, sort, validate
return products.filter { $0.isAvailable }.sorted { $0.name < $1.name }
}
}
// MARK: - Data Layer (Repository)
protocol ProductRepositoryProtocol {
func getProducts() async throws -> [Product]
func getProduct(id: String) async throws -> Product
func saveProduct(_ product: Product) async throws
}
final class ProductRepository: ProductRepositoryProtocol {
private let remoteDataSource: ProductRemoteDataSourceProtocol
private let localDataSource: ProductLocalDataSourceProtocol
init(remoteDataSource: ProductRemoteDataSourceProtocol,
localDataSource: ProductLocalDataSourceProtocol) {
self.remoteDataSource = remoteDataSource
self.localDataSource = localDataSource
}
func getProducts() async throws -> [Product] {
// Try cache first
if let cached = try? await localDataSource.getProducts(), !cached.isEmpty {
// Refresh in background
Task {
if let remote = try? await remoteDataSource.fetchProducts() {
try? await localDataSource.saveProducts(remote)
}
}
return cached
}
// Fetch from remote
let products = try await remoteDataSource.fetchProducts()
try? await localDataSource.saveProducts(products)
return products
}
func getProduct(id: String) async throws -> Product {
try await remoteDataSource.fetchProduct(id: id)
}
func saveProduct(_ product: Product) async throws {
try await remoteDataSource.createProduct(product)
try await localDataSource.saveProduct(product)
}
}
// MARK: - Dependencies Protocol
protocol HasAuthService {
var authService: AuthServiceProtocol { get }
}
protocol HasProductRepository {
var productRepository: ProductRepositoryProtocol { get }
}
typealias AppDependencies = HasAuthService & HasProductRepository
// MARK: - DI Container
final class DependencyContainer: AppDependencies {
// Singletons
lazy var authService: AuthServiceProtocol = AuthService()
// Factories
lazy var productRepository: ProductRepositoryProtocol = {
ProductRepository(
remoteDataSource: ProductRemoteDataSource(apiClient: apiClient),
localDataSource: ProductLocalDataSource(database: database)
)
}()
private lazy var apiClient: APIClientProtocol = APIClient()
private lazy var database: DatabaseProtocol = Database()
// Factory methods for ViewModels
func makeProductListViewModel(coordinator: ProductCoordinator) -> ProductListViewModel {
ProductListViewModel(
getProductsUseCase: GetProductsUseCase(repository: productRepository),
coordinator: coordinator
)
}
}
// MARK: - Property Wrapper Approach
@propertyWrapper
struct Injected<T> {
private let keyPath: KeyPath<DependencyContainer, T>
var wrappedValue: T {
DependencyContainer.shared[keyPath: keyPath]
}
init(_ keyPath: KeyPath<DependencyContainer, T>) {
self.keyPath = keyPath
}
}
// Usage
final class SomeService {
@Injected(\.authService) private var authService
}
// MARK: - View
struct ProductListView: View {
@StateObject private var viewModel: ProductListViewModel
init(viewModel: @autoclosure @escaping () -> ProductListViewModel) {
_viewModel = StateObject(wrappedValue: viewModel())
}
var body: some View {
Group {
if viewModel.isLoading {
ProgressView()
} else if let error = viewModel.error {
ErrorView(error: error) {
Task { await viewModel.loadProducts() }
}
} else {
productList
}
}
.navigationTitle("Products")
.task {
await viewModel.loadProducts()
}
}
private var productList: some View {
List(viewModel.products) { product in
ProductRow(product: product)
.onTapGesture {
viewModel.selectProduct(product)
}
}
}
}
// MARK: - SwiftUI Coordinator (Router)
@MainActor
final class Router: ObservableObject {
@Published var path = NavigationPath()
func push<T: Hashable>(_ value: T) {
path.append(value)
}
func pop() {
path.removeLast()
}
func popToRoot() {
path.removeLast(path.count)
}
}
struct ContentView: View {
@StateObject private var router = Router()
@StateObject private var dependencies = DependencyContainer()
var body: some View {
NavigationStack(path: $router.path) {
ProductListView(viewModel: dependencies.makeProductListViewModel(router: router))
.navigationDestination(for: Product.self) { product in
ProductDetailView(product: product)
}
}
.environmentObject(router)
}
}
| Issue | Cause | Solution |
|---|---|---|
| Massive ViewModel | Too many responsibilities | Split into smaller VMs or use UseCases |
| Tight coupling | Direct dependencies | Use protocols and DI |
| Hard to test | Static/singleton dependencies | Inject dependencies |
| Memory leaks | Strong coordinator references | Use weak delegates |
| State sync issues | Multiple sources of truth | Single source + binding |
// Check retain cycles
deinit {
print("\(Self.self) deinit")
}
// Trace view updates
var body: some View {
let _ = Self._printChanges()
// ...
}
// Validate architecture
// Run: swift package diagnose-api-breaking-changes
validation:
- rule: layer_separation
severity: error
check: Views should not import data layer
- rule: protocol_abstractions
severity: warning
check: Dependencies should be protocols
- rule: unidirectional_flow
severity: info
check: State changes flow in one direction
Skill("swift-architecture")
swift-fundamentals - Protocol-oriented designswift-swiftui - SwiftUI patternsswift-testing - Testing architectureThis skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.