ios-tca/SKILL.md
Implement The Composable Architecture (TCA) by Point-Free for iOS apps with predictable state management, effects, and testing. Use when building with TCA, creating reducers, managing state, handling effects, testing features, or composing features. Triggers on TCA, Composable Architecture, Reducer, Store, Effect, @Reducer, ViewStore, ReducerOf, Point-Free.
npx skillsauth add abanoub-ashraf/manus-skills-import ios-tcaInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 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 The Composable Architecture by Point-Free. When this skill activates, help build robust, testable features using TCA patterns.
| Concept | Purpose | |---------|---------| | State | Data that describes UI at any moment | | Action | Events that can occur (user, system, effects) | | Reducer | Pure function: (State, Action) → State + Effects | | Store | Runtime that holds state and processes actions | | Effect | Side effects (API calls, timers, etc.) |
// Package.swift
dependencies: [
.package(
url: "https://github.com/pointfreeco/swift-composable-architecture",
from: "1.5.0"
)
]
import ComposableArchitecture
@Reducer
struct CounterFeature {
// MARK: - State
@ObservableState
struct State: Equatable {
var count = 0
var isLoading = false
var fact: String?
}
// MARK: - Action
enum Action {
case incrementButtonTapped
case decrementButtonTapped
case factButtonTapped
case factResponse(String)
}
// MARK: - Dependencies
@Dependency(\.numberFact) var numberFact
// MARK: - Reducer
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .incrementButtonTapped:
state.count += 1
return .none
case .decrementButtonTapped:
state.count -= 1
return .none
case .factButtonTapped:
state.isLoading = true
state.fact = nil
return .run { [count = state.count] send in
let fact = try await numberFact.fetch(count)
await send(.factResponse(fact))
}
case .factResponse(let fact):
state.isLoading = false
state.fact = fact
return .none
}
}
}
}
struct CounterView: View {
let store: StoreOf<CounterFeature>
var body: some View {
VStack(spacing: 20) {
HStack(spacing: 20) {
Button("-") {
store.send(.decrementButtonTapped)
}
Text("\(store.count)")
.font(.largeTitle)
Button("+") {
store.send(.incrementButtonTapped)
}
}
Button("Get Fact") {
store.send(.factButtonTapped)
}
.disabled(store.isLoading)
if store.isLoading {
ProgressView()
}
if let fact = store.fact {
Text(fact)
.multilineTextAlignment(.center)
.padding()
}
}
}
}
// Preview
#Preview {
CounterView(
store: Store(initialState: CounterFeature.State()) {
CounterFeature()
}
)
}
import Dependencies
// Protocol
protocol NumberFactClient {
func fetch(_ number: Int) async throws -> String
}
// Live implementation
struct LiveNumberFactClient: NumberFactClient {
func fetch(_ number: Int) async throws -> String {
let (data, _) = try await URLSession.shared
.data(from: URL(string: "http://numbersapi.com/\(number)")!)
return String(decoding: data, as: UTF8.self)
}
}
// Dependency key
extension NumberFactClient: DependencyKey {
static let liveValue: any NumberFactClient = LiveNumberFactClient()
static let testValue: any NumberFactClient = UnimplementedNumberFactClient()
static let previewValue: any NumberFactClient = MockNumberFactClient()
}
extension DependencyValues {
var numberFact: any NumberFactClient {
get { self[NumberFactClient.self] }
set { self[NumberFactClient.self] = newValue }
}
}
// Mock for previews
struct MockNumberFactClient: NumberFactClient {
func fetch(_ number: Int) async throws -> String {
try await Task.sleep(for: .seconds(1))
return "\(number) is a great number!"
}
}
@Reducer
struct AppFeature {
@ObservableState
struct State: Equatable {
var tab1 = CounterFeature.State()
var tab2 = CounterFeature.State()
var selectedTab = 0
}
enum Action {
case tab1(CounterFeature.Action)
case tab2(CounterFeature.Action)
case tabSelected(Int)
}
var body: some ReducerOf<Self> {
Scope(state: \.tab1, action: \.tab1) {
CounterFeature()
}
Scope(state: \.tab2, action: \.tab2) {
CounterFeature()
}
Reduce { state, action in
switch action {
case .tab1, .tab2:
return .none
case .tabSelected(let tab):
state.selectedTab = tab
return .none
}
}
}
}
// View
struct AppView: View {
let store: StoreOf<AppFeature>
var body: some View {
TabView(selection: Binding(
get: { store.selectedTab },
set: { store.send(.tabSelected($0)) }
)) {
CounterView(store: store.scope(state: \.tab1, action: \.tab1))
.tabItem { Text("Tab 1") }
.tag(0)
CounterView(store: store.scope(state: \.tab2, action: \.tab2))
.tabItem { Text("Tab 2") }
.tag(1)
}
}
}
@Reducer
struct ParentFeature {
@ObservableState
struct State: Equatable {
@Presents var detail: DetailFeature.State?
}
enum Action {
case detail(PresentationAction<DetailFeature.Action>)
case showDetailButtonTapped
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .showDetailButtonTapped:
state.detail = DetailFeature.State()
return .none
case .detail(.presented(.closeButtonTapped)):
state.detail = nil
return .none
case .detail:
return .none
}
}
.ifLet(\.$detail, action: \.detail) {
DetailFeature()
}
}
}
// View with sheet
struct ParentView: View {
@Bindable var store: StoreOf<ParentFeature>
var body: some View {
Button("Show Detail") {
store.send(.showDetailButtonTapped)
}
.sheet(item: $store.scope(state: \.detail, action: \.detail)) { store in
DetailView(store: store)
}
}
}
import IdentifiedCollections
@Reducer
struct TodoListFeature {
@ObservableState
struct State: Equatable {
var todos: IdentifiedArrayOf<TodoFeature.State> = []
}
enum Action {
case todos(IdentifiedActionOf<TodoFeature>)
case addTodoButtonTapped
case deleteButtonTapped(id: TodoFeature.State.ID)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .todos:
return .none
case .addTodoButtonTapped:
state.todos.append(TodoFeature.State(id: UUID()))
return .none
case .deleteButtonTapped(let id):
state.todos.remove(id: id)
return .none
}
}
.forEach(\.todos, action: \.todos) {
TodoFeature()
}
}
}
// View
struct TodoListView: View {
let store: StoreOf<TodoListFeature>
var body: some View {
List {
ForEach(store.scope(state: \.todos, action: \.todos)) { todoStore in
TodoRowView(store: todoStore)
}
.onDelete { indexSet in
for index in indexSet {
store.send(.deleteButtonTapped(id: store.todos[index].id))
}
}
}
.toolbar {
Button("Add") {
store.send(.addTodoButtonTapped)
}
}
}
}
@Reducer
struct RootFeature {
@ObservableState
struct State: Equatable {
var path = StackState<Path.State>()
}
enum Action {
case path(StackActionOf<Path>)
case goToProfileButtonTapped
case goToSettingsButtonTapped
}
@Reducer
enum Path {
case profile(ProfileFeature)
case settings(SettingsFeature)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .goToProfileButtonTapped:
state.path.append(.profile(ProfileFeature.State()))
return .none
case .goToSettingsButtonTapped:
state.path.append(.settings(SettingsFeature.State()))
return .none
case .path:
return .none
}
}
.forEach(\.path, action: \.path)
}
}
// View
struct RootView: View {
@Bindable var store: StoreOf<RootFeature>
var body: some View {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
VStack {
Button("Profile") { store.send(.goToProfileButtonTapped) }
Button("Settings") { store.send(.goToSettingsButtonTapped) }
}
} destination: { store in
switch store.case {
case .profile(let store):
ProfileView(store: store)
case .settings(let store):
SettingsView(store: store)
}
}
}
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
// Fire and forget
case .logButtonTapped:
return .run { _ in
await analytics.log("button_tapped")
}
// Send action back
case .loadData:
return .run { send in
let data = try await apiClient.fetchData()
await send(.dataLoaded(data))
}
// Cancellation
case .startTimer:
return .run { send in
for await _ in clock.timer(interval: .seconds(1)) {
await send(.timerTicked)
}
}
.cancellable(id: CancelID.timer)
case .stopTimer:
return .cancel(id: CancelID.timer)
// Debouncing
case .searchTextChanged(let query):
state.searchQuery = query
return .run { send in
try await clock.sleep(for: .milliseconds(300))
let results = try await searchClient.search(query)
await send(.searchResultsLoaded(results))
}
.cancellable(id: CancelID.search, cancelInFlight: true)
// Merge multiple effects
case .refresh:
return .merge(
.send(.loadProfile),
.send(.loadNotifications),
.send(.loadFeed)
)
// Concatenate effects
case .submit:
return .concatenate(
.send(.validate),
.send(.save),
.send(.dismiss)
)
}
}
}
private enum CancelID {
case timer
case search
}
import XCTest
import ComposableArchitecture
@testable import MyApp
final class CounterFeatureTests: XCTestCase {
@MainActor
func test_increment() async {
let store = TestStore(initialState: CounterFeature.State()) {
CounterFeature()
}
await store.send(.incrementButtonTapped) {
$0.count = 1
}
await store.send(.incrementButtonTapped) {
$0.count = 2
}
}
@MainActor
func test_fetchFact() async {
let store = TestStore(initialState: CounterFeature.State(count: 42)) {
CounterFeature()
} withDependencies: {
$0.numberFact.fetch = { number in
"\(number) is the answer!"
}
}
await store.send(.factButtonTapped) {
$0.isLoading = true
$0.fact = nil
}
await store.receive(\.factResponse) {
$0.isLoading = false
$0.fact = "42 is the answer!"
}
}
}
@MainActor
func test_timer() async {
let clock = TestClock()
let store = TestStore(initialState: TimerFeature.State()) {
TimerFeature()
} withDependencies: {
$0.continuousClock = clock
}
await store.send(.startTimer) {
$0.isTimerRunning = true
}
await clock.advance(by: .seconds(1))
await store.receive(\.timerTicked) {
$0.secondsElapsed = 1
}
await clock.advance(by: .seconds(1))
await store.receive(\.timerTicked) {
$0.secondsElapsed = 2
}
await store.send(.stopTimer) {
$0.isTimerRunning = false
}
}
@MainActor
func test_navigation() async {
let store = TestStore(initialState: RootFeature.State()) {
RootFeature()
}
await store.send(.goToProfileButtonTapped) {
$0.path[id: 0] = .profile(ProfileFeature.State())
}
await store.send(.path(.element(id: 0, action: .profile(.backButtonTapped)))) {
$0.path = StackState()
}
}
// Keep state flat when possible
@ObservableState
struct State: Equatable {
var items: IdentifiedArrayOf<Item> = []
var selectedItemID: Item.ID?
}
// Use exhaustive handling
switch action {
case .buttonTapped:
// Handle
}
// Test all state changes
await store.send(.action) {
$0.property = expectedValue
}
// Don't mutate state in effects
return .run { _ in
state.count += 1 // ❌ Won't compile, state is immutable here
}
// Don't forget to handle child actions
case .child:
return .none // ✅ Always return .none for unhandled child actions
// Don't ignore effects in tests
await store.send(.loadData) // ❌ Test will fail if effect sends action
Features/
└── Counter/
├── CounterFeature.swift # Reducer
├── CounterView.swift # SwiftUI View
├── CounterDependencies.swift # Feature-specific deps
└── Tests/
└── CounterFeatureTests.swift
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