# CombineFeedback: unidirectional state machines on top of Combine

> CombineFeedback is a Swift library that applies the ReactiveFeedback architecture to Apple's Combine framework, giving an iOS app a State, Event, Reducer and Feedback split with a Store for the SwiftUI layer. It suits a team that wants one way to change state, and its own documentation says it is a work in progress that is not yet ready for production or commercial use.

**sergdort/CombineFeedback** — Unidirectional reactive architecture using new Apple Combine framework https://developer.apple.com/documentation/combine

- Repository: https://github.com/sergdort/CombineFeedback
- Stars: 686 · Forks: 53
- Language: Swift
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/sergdort-combinefeedback

## The architecture is borrowed, the implementation is not

CombineFeedback is described as a unidirectional reactive architecture using Apple's Combine framework. The description of the repository puts the lineage plainly: it is a Combine implementation of ReactiveFeedback and of RxFeedback, both of which are the same architecture for other reactive stacks.

The lineage matters for evaluation, because the architecture itself is not new and is well understood. The unidirectional pattern is: state changes in one direction only, and the only way to move from one state to the next is to emit an event. A reducer, which is a pure function, applies that event to the state. Feedback decides which event should happen next given the current state. The view sends events and observes state, and never touches the state directly.

The motivation given is that requirements for iOS apps have become huge, and code has to manage a lot of state, for example server responses, cached data, UI state and routing. The library's own warning is worth repeating: reactive programming can help a lot, but in the wrong hands it can do even more harm to a codebase.

So the pitch is not a new way to do reactive programming. It is a way to do one particular shape of it, in a way that keeps the arrows pointing one way, and the project frames itself as a simple and intuitive approach to designing reactive state machines.

The primary language is Swift, the licence is MIT, and the topics list includes swiftui, architecture and unidirectional, which says what the intended consumer is.

## State, Event and Reducer: one arrow, and one place that can move

The three core concepts are described precisely enough to be useful.

State is the single source of truth. It represents a state of your system, is usually a plain Swift type, and is immutable. The only way to transition from one State to another is to emit an Event.

Event represents all possible events that can happen in your system which can cause a transition to a new State.

Reducer is the part that needs the most careful reading, because the documentation goes out of its way to correct a natural assumption. A Reducer is a pure function with a signature of (inout State, Event) -> Void. An Event is not what causes the change. An Event is a representation of the intention to transition. What actually causes the State to change, the embodiment of the corresponding Event, is a Reducer, and a Reducer is the only place where a State can be changed.

The signature is the enforcement. The inout on the State parameter means the reducer mutates the state it is given, and the purity requirement means it does that without touching anything else. Combine that with the fact that a reducer is the only place a state can change, and you get a property that a compiler can partly check: a view cannot assign to the state, because the state it holds is not the same state the reducer mutates.

The Counter example is the whole model in a dozen lines:

```swift
struct Counter: StateMachine {
    struct State {
        var count = 0
    }

    enum Event {
        case increment
        case decrement
    }

    @StateMachineBuilder<State, Event>
    var body: some StateMachine<State, Event> {
        Reducer { state, event in
            switch event {
            case .increment:
                state.count += 1
            case .decrement:
                state.count -= 1
            }
        }
    }
}
```

There is no feedback in that machine, and no side effect, and that is deliberate. A state machine that only counts is a state machine with no effects, and the machine is complete as it stands.

## Feedback is the part that decides what happens next

Feedback fills the gap the other three leave open. While State represents where the system is at a given time, Event represents a state change, and a Reducer is the pure function that enacts the event, there is no type yet to decide which event should take place given a particular current state. That is the job of the Feedback, which is essentially a processing engine, listening to changes in the current State and emitting the corresponding next events.

Two properties follow. Feedbacks do not directly mutate states, and they only emit events which then cause states to change in reducers. So the effect side of the machine cannot corrupt the state, because it never touches it.

The available feedback types are OnChange, OnEvent and SideEffect, with Feedback.custom available for advanced stream composition. OnChange observes a value derived from the state, and the Movies example shows exactly how it is used, including the detail that it observes the initial state, skips repeated values, and cancels in-flight work when the request changes or becomes nil.

That last property is what makes OnChange usable for network work. A derived request that changes means the previous request is stale, and the library cancels it rather than letting two responses race. SideEffect is the other half, and it runs after the reducer.

The Movies machine shows all three pieces wired together:

```swift
    @StateMachineBuilder<State, Event>
    var body: some StateMachine<State, Event> {
        Reducer { state, event in
            switch event {
            case let .didLoad(batch):
                state.movies += batch.results
                state.status = .idle
                state.batch = batch
            case let .didFail(error):
                state.status = .failed(error)
            case .retry, .fetchNext:
                state.status = .loading
            }
        }

        OnChange(of: \.nextPage) { page in
            fetchMovies(page)
                .map(Event.didLoad)
                .catch { Just(Event.didFail($0)) }
        }

        SideEffect { state, event in
            await analytics.track(event, state: state)
        }
    }
```

The dependency is captured as a plain Swift value, fetchMovies of type (Int) -> AnyPublisher<Results, Error>, and the documentation is explicit that dependencies are plain Swift values captured by the machine, by feedback closures, or by service objects, and that CombineFeedback does not require a specific dependency-injection framework.

## Store and StoreBinding are how SwiftUI talks to the machine

The Store is the base class responsible for initialising a UI state machine, and it offers two ways to interact with it. You start a state machine by observing var state: AnyPublisher<State, Never>, and you send input events into it via public final func send(event: Event).

The two are complements. The publisher is for observing, for a SwiftUI view to redraw, and the send method is for the other direction. Initialising it is one line:

```swift
let store = Store(initial: Counter.State(), machine: Counter())
```

And a button that increments the counter is a call to send, with no reference to the state at all:

```swift
Button(action: {
    store.send(event: .increment)
}) {
    return Text("+").font(.largeTitle)
}
```

That is the point of the whole design. The view does not know what the new count will be. It says what the user did, and the machine decides what that means.

The larger example shows the SwiftUI side with @StoreBinding<State, Event> private var state: State, initialised from the store, and the binding used to send an event when a list row appears:

```swift
    var body: some View {
        List {
            ForEach(state.movies.identified(by: \.id)) { movie in
                MovieCell(movie: movie).onAppear {
                // When we reach the end of the list
                // we send `fetchNext` event
                    if self.state.movies.last == movie {
                        self.$state.send(.fetchNext)
                    }
                }
            }
        }
    }
```

Note the split: state.movies is read to render, and self.$state.send is how a new event is raised. The view reads and it sends, and it never writes.

## The project says it is not ready, and says who is doing the work

The warning at the top of the project documentation is the most consequential paragraph in the repository, and any evaluation has to start with it.

It is a work in progress. The library is being actively developed and is not yet ready for production or commercial use. The public API is still evolving and may change without notice between versions. It is currently being polished as the sole author's personal project, and breaking changes should be expected until a stable release is announced.

Each of those claims is checkable against the rest of the repository. The release list has one entry, 0.6.0, described as Beta 6 ready, dated 2019-08-27. So the last published release is more than seven years old, while the last push to the repository was on 2026-09-14. Work is happening and it is not being released under the same tag.

The repository layout supports the same picture. There is Sources/ and Tests/ for the library, CombineFeedback.xcodeproj/ for the Xcode project, Package.swift and Package.resolved for Swift Package Manager, an Example/ directory with several separate sample applications, and a plan.md and a skills/ directory. The examples include a CounterExample, a MoviesExample, a SingleStoreExample, a TrafficLight example and a SignIn example, which is a good sign for a library whose value is in how the pieces fit together.

The licence is MIT, so there is no commercial question about the code itself. The question is whether you want to depend on a 0.x API from a single author that the documentation says will break.

## Where CombineFeedback is the wrong tool

The first limit is stated in the documentation and it is not a technical objection: this is not ready for production or commercial use. A library whose own author says the API may change without notice between versions, and that breaking changes should be expected until a stable release is announced, is a design reference rather than a dependency. That is a perfectly good thing to read and a bad thing to pin.

The second is the platform. Combine is an Apple framework, so this is iOS and macOS code, and a team with a shared core across platforms has to keep the state machine somewhere else, or accept a platform-specific layer.

The third is the size of the idea. The whole model is four types and a builder, and most of what it gives you is a discipline: one place where state changes, one function that decides what to do next, one place where effects live. A team that applies that discipline by hand, with an enum of events, a switch in one file, and a publisher in the view model, gets most of the benefit with no dependency and no Combine requirement in the shared core.

The fourth is the ecosystem question. The repository lists ReactiveFeedback and RxFeedback as the things it implements, and those are the established names in this architecture. If you are evaluating the pattern rather than this library, the older projects have the larger body of examples and the longer record of the sharp edges people hit, and the SwiftUI binding layer here is the part you would most likely have to write yourself for a real app anyway.

The fifth is the release arithmetic. The published version is 0.6.0 from 2019 and the branch has moved since, so the API described in the project documentation is not the API you would install from PyPI-equivalent, in this case from a release tag or the Swift Package Manager resolution of that tag. Depending on the current branch means depending on an unreleased 0.x.

## Conclusion

CombineFeedback is worth looking at for a SwiftUI team that has grown tired of scattered state changes, because the architecture makes the path from an event to a state transition explicit and the StateMachine builder makes reducers and effects impossible to half-wire, and because the Store and StoreBinding pair gives a SwiftUI view a typed way to send events and observe state. It is the wrong choice for anything shipping, because the project states plainly that it is a work in progress, is not ready for production or commercial use, and that breaking changes should be expected until a stable release is announced. Before you adopt it, check the three things that warning covers: the version, since the only release is 0.6.0 from 2019 while the branch is being worked on as of 2026-09-14, so the API in the repository is not the API in the release; the dependency on Combine itself, which means macOS and iOS only; and the fact that it is a sole author's personal project, which the documentation says outright, so there is no team behind it if the API moves again.

## FAQ

### What are the core types in CombineFeedback?

State is the single source of truth and is immutable, Event represents a possible transition, a Reducer is a pure function with the signature (inout State, Event) -> Void and is the only place a State can change, and Feedback is the processing engine that observes the current state and emits the next events. The StateMachine composes reducers and feedbacks in one ordered body.

### How does a SwiftUI view change state with CombineFeedback?

It does not change state directly. The view observes the store's state publisher and raises events with send(event:), and a Reducer applies the event to the state. The SwiftUI side uses @StoreBinding<State, Event> to hold the state and to send events such as .fetchNext when a list row appears.

### Is CombineFeedback ready for production use?

The project documentation says no. It states the library is a work in progress, is not yet ready for production or commercial use, that the public API is still evolving and may change without notice between versions, and that breaking changes should be expected until a stable release is announced, since it is being polished as the sole author's personal project.

## Sources

- [Issues](https://github.com/sergdort/CombineFeedback/issues)
- [License: MIT](https://github.com/sergdort/CombineFeedback/blob/master/LICENSE)
- [README](https://github.com/sergdort/CombineFeedback/blob/master/README.md)
- [Releases](https://github.com/sergdort/CombineFeedback/releases)
- [sergdort/CombineFeedback on GitHub](https://github.com/sergdort/CombineFeedback)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sergdort-combinefeedback
