Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
146: The Composable Architecture Overview
I've been sketching out a small Task Tracker app today, and I hit that familiar wall where my SwiftUI views are starting to feel... bloated. You know the feeling. I have @State variables for the task list, a @State for the text field, and then a bunch of functions inside the View that handle the business logic of adding a task or toggling a completion checkbox.
It works, sure. But the moment I want to add a feature—like syncing these tasks to a server—I realize my logic is trapped inside the View. If I want to test the "add task" logic, I have to instantiate a whole View. That's a red flag. I want a way to describe what happens in my app without tying it to how it's rendered.
The "Everything in the View" Struggle
Here is where I started. Just a basic view doing everything:
struct TaskListView: View {
@State var tasks: [Task] = []
@State var newTaskName: String = ""
var body: some View {
VStack {
TextField("Task name", text: $newTaskName)
Button("Add") {
let task = Task(name: newTaskName)
tasks.append(task)
newTaskName = ""
}
List(tasks) { task in
Text(task.name)
}
}
}
}
It's clean for five minutes. But what happens when adding a task needs to trigger an API call? Or when I need to validate that the name isn't empty? My Button closure is going to turn into a 20-line monster. I need a way to separate the State (the data), the Actions (the things that can happen), and the Reducer (the logic that connects them).
Defining the Blueprint
This is where The Composable Architecture (TCA) comes in. Instead of letting the View mutate state directly, TCA forces us to be explicit. I'm going to stop writing View code and start by defining a "Feature."
First, I'll define my State and my Action. I'm thinking of these as the "Noun" and the "Verb" of my feature.
import ComposableArchitecture
@Reducer
struct TaskFeature {
struct State: Equatable {
var tasks: [Task] = []
var newTaskName: String = ""
}
enum Action {
case nameChanged(String)
case addTask
case toggleTask(id: UUID)
}
var body: some ReducerOf {
Reduce { state, action in
switch action {
case let .nameChanged(name):
state.newTaskName = name
return .none
case .addTask:
guard !state.newTaskName.isEmpty else { return .none }
state.tasks.append(Task(name: state.newTaskName))
state.newTaskName = ""
return .none
case let .toggleTask(id):
if let index = state.tasks.firstIndex(where: { $0.id == id }) {
state.tasks[index].isCompleted.toggle()
}
return .none
}
}
}
}
I noticed something immediately: the Reduce block is just a big switch statement. It's boring, and that's actually the point. There's no magic here. If I want to know why a task was added, I just look for the .addTask case. It's completely decoupled from the UI.
Connecting the Store to the UI
Now, I can't just drop this into a View; I need a Store. The Store is the runtime that actually holds the state and runs the reducer. In my View, I'll use Store`s observer to keep the UI in sync.
I tried using @StateObject at first, but TCA provides a Store that integrates directly with SwiftUI via WithViewStore (or the newer @Bindable` / `ObservedObject` patterns depending on your TCA version). Let's look at the modern approach:
struct TaskListView: View {
let store: StoreOf
var body: some View {
VStack {
TextField("Task name", text: Binding(
get: { store.newTaskName },
set: { store.send(.nameChanged($0)) }
))
Button("Add") {
store.send(.addTask)
}
List(store.tasks) { task in
Text(task.name)
}
}
}
}
Wait, that Binding looks clunky. TCA actually gives us a way to "bind" state directly if we define it in the reducer. But for this overview, the key takeaway is store.send(.action). The View doesn't say "append this to the array"; it says "the user intended to add a task." The Reducer decides if that's actually allowed.
Handling the "Real World" (Effects)
But what about that API call I mentioned? In a standard SwiftUI app, I'd just throw an async call in the button. In TCA, we use Effect. If I want to save the task to a database, I don't do it in the reducer—because reducers must be "pure" (predictable and side-effect free).
I'll adjust my reducer to return an effect:
case .addTask:
let task = Task(name: state.newTaskName)
state.tasks.append(task)
state.newTaskName = ""
return .run { send in
try await API.save(task)
// I could send another action here if the save fails!
}
Now I have a clear pipeline: View $\rightarrow$ Action $\rightarrow$ Reducer $\rightarrow$ Effect $\rightarrow$ Action. It feels like a loop. It's a bit more boilerplate up front, but I can now write a unit test for the TaskFeature without ever launching a simulator. I can just send actions to the store and assert that the state changed as expected.
📋 Practical Task
Implement a Task Deletion Action in TCA
You have been handed a Task Tracker project using The Composable Architecture. The app currently allows users to add and toggle tasks, but it is missing the ability to delete them. Your goal is to implement the deletion flow following the TCA pattern.
Requirements:
- The Action: Add a new case to the
Actionenum in theTaskFeaturereducer calleddeleteTask(id: UUID). - The Logic: Update the
Reduceblock to handle.deleteTask. It should find the task with the matching ID in thestate.tasksarray and remove it. - The View: Update the
Listin the View to include aButton(or use.onDelete) that callsstore.send(.deleteTask(id: task.id)).
Ensure that the state is mutated correctly within the reducer and that the View remains a "dumb" layer that only sends actions.
There are no comments for now.