{"id":18925201,"url":"https://github.com/babylonhealth/reactivefeedback","last_synced_at":"2025-08-19T19:25:12.098Z","repository":{"id":48485191,"uuid":"101814868","full_name":"babylonhealth/ReactiveFeedback","owner":"babylonhealth","description":"Unidirectional reactive architecture","archived":false,"fork":false,"pushed_at":"2023-05-04T13:19:20.000Z","size":234,"stargazers_count":158,"open_issues_count":4,"forks_count":11,"subscribers_count":49,"default_branch":"develop","last_synced_at":"2025-08-17T18:49:36.174Z","etag":null,"topics":["architecture","ios","reactiveswift","redux","swift","unidirectional-data-flow"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/babylonhealth.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2017-08-29T23:09:17.000Z","updated_at":"2025-01-28T04:47:19.000Z","dependencies_parsed_at":"2023-07-14T05:53:14.223Z","dependency_job_id":null,"html_url":"https://github.com/babylonhealth/ReactiveFeedback","commit_stats":null,"previous_names":["babylonpartners/reactivefeedback"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/babylonhealth/ReactiveFeedback","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/babylonhealth%2FReactiveFeedback","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/babylonhealth%2FReactiveFeedback/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/babylonhealth%2FReactiveFeedback/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/babylonhealth%2FReactiveFeedback/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/babylonhealth","download_url":"https://codeload.github.com/babylonhealth/ReactiveFeedback/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/babylonhealth%2FReactiveFeedback/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271207247,"owners_count":24718749,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-19T02:00:09.176Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["architecture","ios","reactiveswift","redux","swift","unidirectional-data-flow"],"created_at":"2024-11-08T11:09:56.340Z","updated_at":"2025-08-19T19:25:12.029Z","avatar_url":"https://github.com/babylonhealth.png","language":"Swift","readme":"# ReactiveFeedback\n\nUnidirectional Reactive Architecture. This is a [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift) implemetation of [RxFeedback](https://github.com/kzaher/RxFeedback)\n\n## Documentation\n\n![](diagrams/ReactiveFeedback.jpg)\n\n### Motivation\n\nRequirements for iOS apps have become huge. Our code has to manage a lot of state e.g. server responses, cached data, UI state, routing etc. Some may say that Reactive Programming can help us a lot but, in the wrong hands, it can do even more harm to your code base.\n\nThe goal of this library is to provide a simple and intuitive approach to designing reactive state machines.\n\n### Core Concepts\n\n##### State\n\n`State` is the single source of truth. It represents a state of your system and is usually a plain Swift type (which doesn't contain any ReactiveSwift primitives). Your state is immutable. The only way to transition from one `State` to another is to emit an `Event`.\n\n```swift\nstruct Results\u003cT: JSONSerializable\u003e {\n    let page: Int\n    let totalResults: Int\n    let totalPages: Int\n    let results: [T]\n\n    static func empty() -\u003e Results\u003cT\u003e {\n        return Results\u003cT\u003e(page: 0, totalResults: 0, totalPages: 0, results: [])\n    }\n}\n\nstruct Context {\n    var batch: Results\u003cMovie\u003e\n    var movies: [Movie]\n\n    static var empty: Context {\n        return Context(batch: Results.empty(), movies: [])\n    }\n}\n\nenum State {\n    case initial\n    case paging(context: Context)\n    case loadedPage(context: Context)\n    case refreshing(context: Context)\n    case refreshed(context: Context)\n    case error(error: NSError, context: Context)\n    case retry(context: Context)\n}\n```\n\n##### Event\n\nRepresents all possible events that can happen in your system which can cause a transition to a new `State`.\n\n```swift\nenum Event {\n    case startLoadingNextPage\n    case response(Results\u003cMovie\u003e)\n    case failed(NSError)\n    case retry\n}\n```\n\n##### Reducer\n\nA Reducer is a pure function with a signature of `(State, Event) -\u003e State`. While `Event` represents an action that results in a `State` change, it's actually not what _causes_ the change. An `Event` is just that, a representation of the intention to transition from one state to another. What actually causes the `State` to change, the embodiment of the corresponding `Event`, is a Reducer. A Reducer is the only place where a `State` can be changed.\n\n```swift\nstatic func reduce(state: State, event: Event) -\u003e State {\n    switch event {\n    case .startLoadingNextPage:\n        return .paging(context: state.context)\n    case .response(let batch):\n        var copy = state.context\n        copy.batch = batch\n        copy.movies += batch.results\n        return .loadedPage(context: copy)\n    case .failed(let error):\n        return .error(error: error, context: state.context)\n    case .retry:\n        return .retry(context: state.context)\n    }\n}\n```\n\n##### Feedback\n\nWhile `State` represents where the system is at a given time, `Event` represents a trigger for state change, and a `Reducer` is the pure function that changes the state depending on current state and type of event received, there is not as of yet any type to emit events given a particular current state. That's the job of the `Feedback`. It's essentially a \"processing engine\", listening to changes in the current `State` and emitting the corresponding next events to take place. It's represented by a pure function with a signature of `Signal\u003cState, NoError\u003e -\u003e Signal\u003cEvent, NoError\u003e`. Feedbacks don't directly mutate states. Instead, they only emit events which then cause states to change in reducers.\n\n```swift\npublic struct Feedback\u003cState, Event\u003e {\n    public let events: (Scheduler, Signal\u003cState, NoError\u003e) -\u003e Signal\u003cEvent, NoError\u003e\n}\n\nfunc loadNextFeedback(for nearBottomSignal: Signal\u003cVoid, NoError\u003e) -\u003e Feedback\u003cState, Event\u003e {\n    return Feedback(predicate: { !$0.paging }) { _ in\n        return nearBottomSignal\n            .map { Event.startLoadingNextPage }\n        }\n}\n\nfunc pagingFeedback() -\u003e Feedback\u003cState, Event\u003e {\n    return Feedback\u003cState, Event\u003e(skippingRepeated: { $0.nextPage }) { (nextPage) -\u003e SignalProducer\u003cEvent, NoError\u003e in\n        return URLSession.shared.fetchMovies(page: nextPage)\n            .map(Event.response)\n            .flatMapError { (error) -\u003e SignalProducer\u003cEvent, NoError\u003e in\n                return SignalProducer(value: Event.failed(error))\n            }\n        }\n}\n\nfunc retryFeedback(for retrySignal: Signal\u003cVoid, NoError\u003e) -\u003e Feedback\u003cState, Event\u003e {\n    return Feedback\u003cState, Event\u003e(skippingRepeated: { $0.lastError }) { _ -\u003e Signal\u003cEvent, NoError\u003e in\n        return retrySignal.map { Event.retry }\n    }\n}\n\nfunc retryPagingFeedback() -\u003e Feedback\u003cState, Event\u003e {\n    return Feedback\u003cState, Event\u003e(skippingRepeated: { $0.retryPage }) { (nextPage) -\u003e SignalProducer\u003cEvent, NoError\u003e in\n        return URLSession.shared.fetchMovies(page: nextPage)\n            .map(Event.response)\n            .flatMapError { (error) -\u003e SignalProducer\u003cEvent, NoError\u003e in\n                return SignalProducer(value: Event.failed(error))\n            }\n        }\n}\n```\n\n### The Flow\n\n1. As you can see from the diagram above we always start with an initial state.\n2. Every change to the `State` will be then delivered to all `Feedback` loops that were added to the system.\n3. `Feedback` then decides whether any action should be performed with a subset of the `State` (e.g calling API, observe UI events) by dispatching an `Event`, or ignoring it by returning `SignalProducer.empty`.\n4. Dispatched `Event` then goes to the `Reducer` which applies it and returns a new value of the `State`.\n5. And then cycle starts all over (see 2).\n\n##### Example\n```swift\nlet increment = Feedback\u003cInt, Event\u003e { _ in\n    return self.plusButton.reactive\n        .controlEvents(.touchUpInside)\n        .map { _ in Event.increment }\n}\n\nlet decrement = Feedback\u003cInt, Event\u003e { _ in\n    return self.minusButton.reactive\n        .controlEvents(.touchUpInside)\n        .map { _ in Event.decrement }\n}\n\nlet system = SignalProducer\u003cInt, NoError\u003e.system(initial: 0,\n    reduce: { (count, event) -\u003e Int in\n        switch event {\n        case .increment:\n            return count + 1\n        case .decrement:\n            return count - 1\n        }\n    },\n    feedbacks: [increment, decrement])\n\nlabel.reactive.text \u003c~ system.map(String.init)\n```\n\n![](diagrams/increment_example.gif)\n\n### Advantages\n\nTODO\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbabylonhealth%2Freactivefeedback","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbabylonhealth%2Freactivefeedback","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbabylonhealth%2Freactivefeedback/lists"}