swift-architecture
Select, implement, or migrate between app architecture patterns for Apple platform apps. Use when choosing between MV (Model-View with @Observable), MVVM, MVI, TCA (The Composable Architecture), Clean Architecture, VIPER, or Coordinator patterns; when evaluating architecture fit for a feature's complexity; when migrating from one pattern to another; or when reviewing whether an app's current architecture is appropriate. Scoped to Apple-platform patterns using Swift 6.3, SwiftUI, and UIKit.
What this skill does
# Swift Architecture
Select and implement the right architecture pattern for Apple platform apps built with Swift 6.3 and SwiftUI or UIKit.
## Contents
- [Scope Boundary](#scope-boundary)
- [Architecture Selection](#architecture-selection)
- [MV Pattern (Model-View with `@Observable`)](#mv-pattern)
- [MVVM](#mvvm)
- [MVI (Model-View-Intent)](#mvi)
- [TCA (The Composable Architecture)](#tca)
- [Clean Architecture](#clean-architecture)
- [Coordinator Pattern](#coordinator-pattern)
- [VIPER](#viper)
- [Migration Between Patterns](#migration-between-patterns)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)
## Scope Boundary
This skill owns architecture-level decisions: pattern selection, module
boundaries, dependency direction, migration/escalation strategy, and structural
test strategy. It does not own SwiftUI state mechanics; route `@State`,
`@Bindable`, `@Environment`, edit-sheet/local state, bindings, view composition,
and `@Observable` MV implementation mechanics to `swiftui-patterns`. Use
`swiftui-navigation` for `NavigationStack`, `NavigationSplitView`,
`NavigationPath`, route models, sheets, tabs, and deep-link URL handling;
`swift-concurrency` for `@MainActor`, default MainActor isolation, `Sendable`,
strict-concurrency diagnostics, and data-race diagnostics; and `swift-testing`
for `@Test`, `#expect`, `#require`, fixtures, parameterized tests, mocks, stubs,
and suite organization.
## Architecture Selection
| Pattern | Best For | Complexity | Testability |
|---------|----------|-----------|-------------|
| **MV** | Small-to-medium SwiftUI apps, rapid iteration | Low | Moderate |
| **MVVM** | Medium apps, teams familiar with reactive patterns | Medium | High |
| **MVI** | Complex state machines, predictable state flow | Medium-High | High |
| **TCA** | Large apps needing composable features, strong testing | High | Very High |
| **Clean Architecture** | Enterprise apps, strict separation of concerns | High | Very High |
| **Coordinator** | Apps with complex navigation flows (UIKit or hybrid) | Medium | High |
| **VIPER** | Legacy UIKit modules already using VIPER boundaries | Very High | High |
**Default recommendation for new SwiftUI apps:** Start with MV (Model-View
with `@Observable`). Escalate to MVVM or TCA only when the feature's complexity
demands it.
Boundary-split answers should use one `swift-architecture` bucket for
pattern/module/dependency/migration/test-strategy decisions. Do not add a
separate architecture-owned "SwiftUI state ownership" bucket; property-wrapper,
local binding, navigation, concurrency-diagnostic, fixture, and parameterized
test mechanics are sibling-skill handoffs.
### Decision Framework
1. **Is the feature a simple CRUD screen?** → MV pattern
2. **Does the screen have complex business logic separate from the view?** → MVVM
3. **Do you need deterministic state transitions and side-effect management?** → MVI or TCA
4. **Is the app large with many independent feature modules?** → TCA or Clean Architecture
5. **Is navigation complex with deep linking and conditional flows?** → Add Coordinator pattern
## MV Pattern
The simplest SwiftUI architecture. The view observes `@Observable` models
directly. No intermediate view model layer.
```swift
import Observation
import SwiftUI
@MainActor
@Observable
final class TripStore {
var trips: [Trip] = []
var isLoading = false
var error: Error?
private let service: TripService
init(service: TripService) {
self.service = service
}
func loadTrips() async {
isLoading = true
defer { isLoading = false }
do {
trips = try await service.fetchTrips()
} catch {
self.error = error
}
}
func deleteTrip(_ trip: Trip) async throws {
try await service.delete(trip)
trips.removeAll { $0.id == trip.id }
}
}
struct TripsView: View {
@State private var store = TripStore(service: .live)
var body: some View {
List(store.trips) { trip in
TripRow(trip: trip)
}
.task { await store.loadTrips() }
}
}
```
**When MV is enough:** Single-screen features, prototype/MVP, small teams,
straightforward data flow.
**When to upgrade:** Business logic grows complex, unit testing the view's
behavior becomes difficult, multiple views need to share and transform the
same state differently.
## MVVM
Separates view logic into a `ViewModel` that the view observes. The view model
transforms model data for display and handles user actions.
```swift
@MainActor
@Observable
final class TripListViewModel {
private(set) var trips: [TripRowItem] = []
private(set) var isLoading = false
var searchText = ""
var filteredTrips: [TripRowItem] {
guard !searchText.isEmpty else { return trips }
return trips.filter { $0.name.localizedStandardContains(searchText) }
}
private let repository: TripRepository
init(repository: TripRepository) {
self.repository = repository
}
func loadTrips() async {
isLoading = true
defer { isLoading = false }
let models = (try? await repository.fetchAll()) ?? []
trips = models.map { TripRowItem(from: $0) }
}
func delete(at offsets: IndexSet) async {
let toDelete = offsets.map { filteredTrips[$0] }
for item in toDelete {
try? await repository.delete(id: item.id)
}
await loadTrips()
}
}
struct TripRowItem: Identifiable {
let id: UUID
let name: String
let dateRange: String
init(from trip: Trip) {
self.id = trip.id
self.name = trip.name
self.dateRange = trip.startDate.formatted(.dateTime.month().day())
+ " – " + trip.endDate.formatted(.dateTime.month().day())
}
}
struct TripListView: View {
@State private var viewModel: TripListViewModel
init(repository: TripRepository) {
_viewModel = State(initialValue: TripListViewModel(repository: repository))
}
var body: some View {
List {
ForEach(viewModel.filteredTrips) { item in
Text(item.name)
}
.onDelete { offsets in
Task { await viewModel.delete(at: offsets) }
}
}
.searchable(text: $viewModel.searchText)
.task { await viewModel.loadTrips() }
}
}
```
**Testing a ViewModel:**
```swift
@Test func filteredTripsMatchesSearch() async {
let repo = MockTripRepository(trips: [
Trip(name: "Paris"), Trip(name: "Tokyo"), Trip(name: "Paris TX")
])
let vm = TripListViewModel(repository: repo)
await vm.loadTrips()
vm.searchText = "Paris"
#expect(vm.filteredTrips.count == 2)
}
```
## MVI
Unidirectional data flow: views dispatch **intents**, a **reducer** produces
new **state**, and **side effects** are handled explicitly.
```swift
@MainActor
@Observable
final class TripListStore {
private(set) var state = State()
struct State {
var trips: [Trip] = []
var isLoading = false
var error: String?
}
enum Intent {
case loadTrips
case deleteTrip(Trip)
case clearError
}
private let service: TripService
init(service: TripService) {
self.service = service
}
func send(_ intent: Intent) {
Task { await handle(intent) }
}
private func handle(_ intent: Intent) async {
switch intent {
case .loadTrips:
state.isLoading = true
do {
state.trips = try await service.fetchTrips()
} catch {
state.error = error.localizedDescription
}
state.isLoading = false
case .deleteTrip(let trip):
try? await service.delete(trip)
state.trips.removeAll { $0.id == trip.id }
case .clearError:
state.error = nil
}
}Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.