| name | swift-architecture |
| description | Master iOS/macOS app architecture - MVVM, Clean Architecture, Coordinator, DI, Repository |
| version | 2.0.0 |
| sasmp_version | 1.3.0 |
| bonded_agent | 05-swift-macos |
| bond_type | SECONDARY_BOND |
Swift Architecture Skill
Design patterns and architectural approaches for scalable, testable Swift applications.
Prerequisites
- Understanding of SOLID principles
- Familiarity with dependency injection
- Experience with protocol-oriented programming
Parameters
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
Topics Covered
Architecture Patterns
| 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 |
Key Principles
| 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 Responsibilities
| Layer | Responsibility |
|---|
| View | UI rendering only |
| ViewModel | Presentation logic |
| UseCase | Business logic |
| Repository | Data access |
| Service | External integrations |
Code Examples
MVVM with Coordinator
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 }
}
}
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
}
() {
dependencies.authService.isLoggedIn {
showMain()
} {
showLogin()
}
}
() {
coordinator (
navigationController: navigationController,
dependencies: dependencies
)
coordinator.delegate
addChild(coordinator)
coordinator.start()
}
() {
coordinator (
navigationController: navigationController,
dependencies: dependencies
)
addChild(coordinator)
coordinator.start()
}
}
: {
( : ) {
removeChild(coordinator)
showMain()
}
}
: {
products: [] { }
isLoading: { }
error: ? { }
()
( : )
}
: {
products: [] []
isLoading
error: ?
getProductsUseCase:
coordinator: ?
(: , : ) {
.getProductsUseCase getProductsUseCase
.coordinator coordinator
}
() {
isLoading
error
{
products getProductsUseCase.execute()
} {
.error error
}
isLoading
}
( : ) {
coordinator.showProductDetail(product)
}
}
Clean Architecture Layers
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()
return products.filter { $0.isAvailable }.sorted { $0.name < $1.name }
}
}
protocol ProductRepositoryProtocol {
func getProducts() async throws -> [Product]
func getProduct(id: String) async throws -> Product
( : )
}
: {
remoteDataSource:
localDataSource:
(: ,
: ) {
.remoteDataSource remoteDataSource
.localDataSource localDataSource
}
() -> [] {
cached localDataSource.getProducts(), cached.isEmpty {
{
remote remoteDataSource.fetchProducts() {
localDataSource.saveProducts(remote)
}
}
cached
}
products remoteDataSource.fetchProducts()
localDataSource.saveProducts(products)
products
}
(: ) -> {
remoteDataSource.fetchProduct(id: id)
}
( : ) {
remoteDataSource.createProduct(product)
localDataSource.saveProduct(product)
}
}
Dependency Injection Container
protocol HasAuthService {
var authService: AuthServiceProtocol { get }
}
protocol HasProductRepository {
var productRepository: ProductRepositoryProtocol { get }
}
typealias AppDependencies = HasAuthService & HasProductRepository
final class DependencyContainer: AppDependencies {
lazy var authService: AuthServiceProtocol = AuthService()
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()
(: ) -> {
(
getProductsUseCase: (repository: productRepository),
coordinator: coordinator
)
}
}
<> {
keyPath: <, >
wrappedValue: {
.shared[keyPath: keyPath]
}
( : <, >) {
.keyPath keyPath
}
}
{
(\.authService) authService
}
SwiftUI MVVM
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)
}
}
}
}
@MainActor
final class Router: ObservableObject {
path ()
<: >( : ) {
path.append(value)
}
() {
path.removeLast()
}
() {
path.removeLast(path.count)
}
}
: {
router ()
dependencies ()
body: {
(path: .path) {
(viewModel: dependencies.makeProductListViewModel(router: router))
.navigationDestination(for: .) { product
(product: product)
}
}
.environmentObject(router)
}
}
Troubleshooting
Common Issues
| 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 |
Debug Tips
deinit {
print("\(Self.self) deinit")
}
var body: some View {
let _ = Self._printChanges()
}
Validation Rules
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
Usage
Skill("swift-architecture")
Related Skills
swift-fundamentals - Protocol-oriented design
swift-swiftui - SwiftUI patterns
swift-testing - Testing architecture