Unidirectional Reactive Architecture. This is a ReactiveSwift counterpart of RxFeedback.
Requirements 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.
The goal of this library is to provide a simple and intuitive approach to designing reactive state machines.
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.
structResults<T:JSONSerializable>{letpage:IntlettotalResults:IntlettotalPages:Intletresults:[T]staticfunc empty()->Results<T>{returnResults<T>(page:0, totalResults:0, totalPages:0, results:[])}}structContext{varbatch:Results<Movie>varmovies:[Movie]staticvarempty:Context{returnContext(batch:Results.empty(), movies:[])}}enumState{case initial
case paging(context:Context)case loadedPage(context:Context)case refreshing(context:Context)case refreshed(context:Context)case error(error:NSError, context:Context)case retry(context:Context)}Represents all possible events that can happen in your system which can cause a transition to a new State.
enumEvent{case startLoadingNextPage
case response(Results<Movie>)case failed(NSError)case retry
}A Reducer is a pure function with a signature of (State, Event) -> 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.
staticfunc reduce(state:State, event:Event)->State{switch event {case.startLoadingNextPage:return.paging(context: state.context)case.response(let batch):varcopy= state.context
copy.batch = batch
copy.movies += batch.results
return.loadedPage(context: copy)case.failed(let error):return.error(error: error, context: state.context)case.retry:return.retry(context: state.context)}}While 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<State, NoError> -> Signal<Event, NoError>. Feedbacks don't directly mutate states. Instead, they only emit events which then cause states to change in reducers.
publicstructFeedback<State, Event>{publicletevents:(Scheduler,Signal<State,NoError>)->Signal<Event,NoError>}func loadNextFeedback(for nearBottomSignal:Signal<Void,NoError>)->Feedback<State,Event>{returnFeedback(predicate:{ !$0.paging }){ _ inreturn nearBottomSignal
.map{Event.startLoadingNextPage }}}func pagingFeedback()->Feedback<State,Event>{returnFeedback<State,Event>(skippingRepeated:{ $0.nextPage }){(nextPage)->SignalProducer<Event,NoError>inreturnURLSession.shared.fetchMovies(page: nextPage).map(Event.response).flatMapError{(error)->SignalProducer<Event,NoError>inreturnSignalProducer(value:Event.failed(error))}}}func retryFeedback(for retrySignal:Signal<Void,NoError>)->Feedback<State,Event>{returnFeedback<State,Event>(skippingRepeated:{ $0.lastError }){ _ ->Signal<Event,NoError>inreturn retrySignal.map{Event.retry }}}func retryPagingFeedback()->Feedback<State,Event>{returnFeedback<State,Event>(skippingRepeated:{ $0.retryPage }){(nextPage)->SignalProducer<Event,NoError>inreturnURLSession.shared.fetchMovies(page: nextPage).map(Event.response).flatMapError{(error)->SignalProducer<Event,NoError>inreturnSignalProducer(value:Event.failed(error))}}}- As you can see from the diagram above we always start with an initial state.
- Every change to the
Statewill be then delivered to allFeedbackloops that were added to the system. Feedbackthen decides whether any action should be performed with a subset of theState(e.g calling API, observe UI events) by dispatching anEvent, or ignoring it by returningSignalProducer.empty.- Dispatched
Eventthen goes to theReducerwhich applies it and returns a new value of theState. - And then cycle starts all over (see 2).
letincrement=Feedback<Int,Event>{ _ inreturnself.plusButton.reactive
.controlEvents(.touchUpInside).map{ _ inEvent.increment }}letdecrement=Feedback<Int,Event>{ _ inreturnself.minusButton.reactive
.controlEvents(.touchUpInside).map{ _ inEvent.decrement }}letsystem=SignalProducer<Int,NoError>.system(initial:0,
reduce:{(count, event)->Intinswitch event {case.increment:return count +1case.decrement:return count -1}},
feedbacks:[increment, decrement])
label.reactive.text <~ system.map(String.init)TODO
This is a community fork of the ReactiveFeedback project (with the MIT license) from Babylon Health.


