Spin aims to provide a versatile Feedback Loop implementation working with the three main reactive frameworks available in the Swift community (RxSwift, ReactiveSwift and Combine)

Overview

Swift Package Manager compatible Carthage Compatible CocoaPods Compatible platforms codecov

Spin Logo

With the introduction of Combine and SwiftUI, we will face some transition periods in our code base. Our applications will use both Combine and a third-party reactive framework, or both UIKit and SwiftUI, which makes it potentially difficult to guarantee a consistent architecture over time.

Spin is a tool to build feedback loops within a Swift based application allowing you to use a unified syntax whatever the underlying reactive programming framework and whatever Apple UI technology you use (RxSwift, ReactiveSwift, Combine and UIKit, AppKit, SwiftUI).

Please dig into the Demo applications if you already feel comfortable with the feedback loop theory.

Summary:

Change Log

Please read the CHANGELOG.md for information about evolutions and breaking changes.

About State Machines

What is a State Machine?

It's an abstract machine that can be in exactly one of a finite number of states at any given time. The state machine can change from one state to another in response to some external inputs. The change from one state to another is called a transition. A state machine is defined by a list of its states, its initial state, and the conditions for each transition

Guess what! An application IS a state machine.

We just have to find the right tool to implement it. This is where feedback loops come into play 👍 .

A Feedback Loop is a system that is able to self-regulate by using the resulting value from its computations as the next input to itself, constantly adjusting this value according to given rules (Feedback Loops are used in domains like electronics to automatically adjust the level of a signal for instance).

Feedback Loop

Stated this way might sound obscur and unrelated to software engineering, BUT “adjusting a value according to certain rules” is exactly what a program, and by extension an application, is made for! An application is the sum of all kinds of states that we want to regulate to provide a consistent behaviour following precise rules.

Feedback loops are perfect candidates to host and manage state machines inside an application.

About Spin

Spin is a tool whose only purpose is to help you build feedback loops called "Spins". A Spin is based on three components: an initial state, several feedbacks, and a reducer. To illustrate each one of them, we will rely on a basic example: a “feedback loop / Spin” that counts from 0 to 10.

  • The initial state: this is the starting value of our counter, 0.
  • A feedback: this is the rule we apply to the counter to accomplish our purpose. If 0 <= counter < 10 then we ask to increase the counter else we ask to stop it.
  • A reducer: this is the state machine of our Spin. It describes all the possible transitions of our counter given its previous value and the request computed by the feedback. For instance: if the previous value was 0 and the request is to increase it, then the new value is 1, if the previous was 1 and the request is to increase it, then the new value is 2, and so on and so on. When the request from the feedback is to stop, then the previous value is returned as the new value.

Feedback Loop

Feedbacks are the only place where you can perform side effects (networking, local I/O, UI rendering, whatever you do that accesses or mutates a state outside the local scope of the loop). Conversely, a reducer is a pure function that can only produce a new value given a previous one and a transition request. Performing side effects in reducers is forbidden, as it would compromise its reproducibility.

In real life applications, you can obviously have several feedbacks per Spin in order to separate concerns. Each of the feedbacks will be applied sequentially on the input value.

The multiple ways to build a Spin

Spin offers two ways to build a feedback loop. Both are equivalent and picking one depends only on your preference.

Let’s try them by building a Spin that regulates two integer values to make them converge to their average value (like some kind of system that would adjust a left and a right channel volume on stereo speakers to make them converge to the same level).

The following example will rely on RxSwift, here are the ReactiveSwift and Combine counterparts; you will see how similar they are.

We will need a data type for our state:

struct Levels {
    let left: Int
    let right: Int
}

We will also need a data type to describe the transitions to perform on Levels:

enum Event {
    case increaseLeft
    case decreaseLeft 
    case increaseRight
    case decreaseRight
}

Now we can write the two feedbacks that will have an effect on each level:

func leftEffect(inputLevels: Levels) -> Observable<Event> {
    // this is the stop condition to our Spin
    guard inputLevels.left != inputLevels.right else { return .empty() }

    // this is the regulation for the left level
    if inputLevels.left < inputLevels.right {
        return .just(.increaseLeft)
    }  else {
        return .just(.decreaseLeft)
    }
}

func rightEffect(inputLevels: Levels) -> Observable<Event> {
    // this is the stop condition to our Spin
    guard inputLevels.left != inputLevels.right else { return .empty() }

    // this is the regulation for the right level
    if inputLevels.right < inputLevels.left {
        return .just(.increaseRight)
    }  else {
        return .just(.decreaseRight)
    }
}

And finally to describe the state machine ruling the transitions, we need a reducer:

func levelsReducer(currentLevels: Levels, event: Event) -> Levels {

	guard currentLevels.left != currentLevels.right else { return currentLevels }

	switch event {
	    case .decreaseLeft:
	        return Levels(left: currentLevels.left-1, right: currentLevels.right)
	    case .increaseLeft:
	        return Levels(left: currentLevels.left+1, right: currentLevels.right)
	    case .decreaseRight:
	        return Levels(left: currentLevels.left, right: currentLevels.right-1)
	    case .increaseRight:
	        return Levels(left: currentLevels.left, right: currentLevels.right+1)
	}
}

The builder way

In that case, the “Spinner” class is your entry point.

let levelsSpin = Spinner
    .initialState(Levels(left: 10, right: 20))
    .feedback(Feedback(effect: leftEffect))
    .feedback(Feedback(effect: rightEffect))
    .reducer(Reducer(levelsReducer))

That’s it. The feedback loop is built. What now?

If you want to start it, then you have to subscribe to the underlying reactive stream. To that end, a new operator “.stream(from:)” has been added to Observable in order to connect things together and provide an Observable you can subscribe to:

Observable
    .stream(from: levelsSpin)
    .subscribe()
    .disposed(by: self.disposeBag)

There is a shortcut function to directly subscribe to the underlying stream:

Observable
    .start(spin: levelsSpin)
    .disposed(by: self.disposeBag)

For instance, the same Spin using Combine would be (considering the effects return AnyPublishers):

let levelsSpin = Spinner
    .initialState(Levels(left: 10, right: 20))
    .feedback(Feedback(effect: leftEffect))
    .feedback(Feedback(effect: rightEffect))
    .reducer(Reducer(levelsReducer))
	
AnyPublisher
    .stream(from: levelsSpin)
    .sink(receiveCompletion: { _ in }, receiveValue: { _ in })
    .store(in: &cancellables)
	
or

AnyPublisher
    .start(spin: levelsSpin)
    .store(in: &cancellables)

The declarative way

In this case we use a "DSL like" syntax thanks to Swift 5.1 function builder:

let levelsSpin = Spin(initialState: Levels(left: 10, right: 20)) {
    Feedback(effect: leftEffect)
    Feedback(effect: rightEffect)
    Reducer(levelsReducer)
}

Again, with Combine, same syntax considering that effects return AnyPublishers:

let levelsSpin = Spin(initialState: Levels(left: 10, right: 20)) {
    Feedback(effect: leftEffect)
    Feedback(effect: rightEffect)
    Reducer(levelsReducer)
}

The way to start the Spin remains unchanged.

The multiple ways to create a Feedback

As you saw, a “Feedback loop / Spin” is created from several feedbacks. A feedback is a wrapper structure around a side effect function. Basically, a side effect has this signature (Stream<State>) -> Stream<Event>, Stream being a reactive stream (Observable, SignalProducer or AnyPublisher).

As it might not always be easy to directly manipulate Streams, Spin comes with a bunch of helper constructors for feedbacks allowing to:

  • directly receive a State instead of a Stream (like in the example with the Levels)
  • filter the input State by providing a predicate: RxFeedback(effect: leftEffect, filteredBy: { $0.left > 0 })
  • extract a substate from the State by providing a lens or a keypath: RxFeedback(effect: leftEffect, lensingOn: \.left)

Please refer to FeedbackDefinition+Default.swift for completeness.

Feedback lifecycle

There are typical cases where a side effect consist of an asynchronous operation (like a network call). What happens if the very same side effect is called repeatedly, not waiting for the previous ones to end? Are the operations stacked? Are they cancelled when a new one is performed?

Well, it depends 😁 . By default, Spin will cancel the previous operation. But there is a way to override this behaviour. Every feedback constructor that takes a State as a parameter can also be passed an ExecutionStrategy:

  • .cancelOnNewState, to cancel the previous operation when a new state is to be handled
  • .continueOnNewState, to let the previous operation naturally end when a new state is to be handled

Choose wisely the option that fits your needs. Not cancelling previous operations could lead to inconsistency in your state if the reducer is not protected against unordered events.

Feedbacks and scheduling

Reactive programming is often associated with asynchronous execution. Even though every reactive framework comes with its own GCD abstraction, it is always about stating which scheduler the side effect should be executed on.

By default, a Spin will be executed on a background thread created by the framework.

However, Spin provides a way to specify a scheduler for the Spin it-self and for each feedback you add to it:

Spinner
    .initialState(Levels(left: 10, right: 20), executeOn: MainScheduler.instance)
    .feedback(Feedback(effect: leftEffect, on: SerialDispatchQueueScheduler(qos: .userInitiated)))
    .feedback(Feedback(effect: rightEffect, on: SerialDispatchQueueScheduler(qos: .userInitiated)))
    .reducer(Reducer(levelsReducer))

or

Spin(initialState: Levels(left: 10, right: 20), executeOn: MainScheduler.instance) {
    Feedback(effect: leftEffect)
        .execute(on: SerialDispatchQueueScheduler(qos: .userInitiated))
    Feedback(effect: rightEffect)
        .execute(on: SerialDispatchQueueScheduler(qos: .userInitiated))
    Reducer(levelsReducer)
}

Of course, it remains possible to handle the Schedulers by yourself inside the feedback functions.

What about dependencies in Feedbacks

As we saw, a Feedback is a wrapper around a side effect. Side effects, by definition, will need some dependencies to perform their work. Things like: a network service, some persistence tools, a cryptographic utility and so on.

However, the side effect signature doesn't allow to pass dependencies, only a state. How can we take those deps into account ?

These are three possible technics:

1: Use a container type

class MyUseCase {
    private let networkService: NetworkService
    private let cryptographicTool: CryptographicTool
    
    init(networkService: NetworkService, cryptographicTool: CryptographicTool) {
        self.networkService = networkService
        self.cryptographicTool = cryptographicTool
    }
    
    func load(state: MyState) -> AnyPublisher<MyEvent, Never> {
        guard state == .loading else return { Empty().eraseToAnyPublisher() }
        
        // use the deps here
        self.networkService
            .fetch()
            .map { [cryptographicTool] in cryptographicTool.decrypt($0) }
            ...
    }
}

// then we can build a Feedback with this UseCase
let myUseCase = MyUseCase(networkService: MyNetworkService(), cryptographicTool: MyCryptographicTool())
let feedback = Feedback(effect: myUseCase.load)

This technic has the benefit to be very familiar in terms of conception and could be compatible with existing patterns in your application.

It has the downside of forcing us to be careful while capturing dependencies in the side effect.

2: Use a feedback factory function

In the previous technic, we use the MyUseCase only as a container of dependencies. It has no other usage than that. We can get rid of it by using a function (global or static) that will receive our dependencies and help capture them in the side effect:

typealias LoadEffect: (MyState) -> AnyPublisher<MyEvent, Never>
func makeLoadEffect(networkService: NetworkService, cryptographicTool: CryptographicTool) -> LoadEffect {
   return { state in
       guard state == .loading else return { Empty().eraseToAnyPublisher() }
       
       networkService
            .fetch()
            .map { cryptographicTool.decrypt($0) }
            ...
   }
} 

// then we can build a Feedback using this factory function
let effect = makeLoadEffect(networkService: MyNetworkService(), cryptographicTool: MyCryptographicTool())
let feedback = Feedback(effect: effect)

3: Use the built-in Feedback initializer

Spin comes with some Feedback initializers that ease the injection of dependencies. Under the hood, it uses a generic technic derived from the above one.

func loadEffect(networkService: NetworkService,
                cryptographicTool: CryptographicTool,
                state: MyState) -> AnyPublisher<MyEvent, Never> {
    guard state == .loading else return { Empty().eraseToAnyPublisher() }
    
    networkService
            .fetch()
            .map 
}

// then we can build a Feedback directly using the appropriate initializer
let feedback = Feedback(effect: effect, dep1: MyNetworkService(), dep2: MyCryptographicTool())

Among those 3 technics it is the less verbose one. It feels a little bit like magic but simply uses partialization under the hood.

Using Spin in a UIKit or AppKit based app

Although a feedback loop can exist by itself without any visualization, it makes more sense in our developer world to use it as a way to produce a State that we be rendered on screen and to handle events emitted by the users.

Fortunately, taking a State as an input for rendering and returning a stream of events from the user interactions looks A LOT like the definition of a feedback (State -> Stream<Event>), we know how to handle feedbacks 😁 , with a Spin of course.

As the view is a function of a State, rendering it will change the states of the UI elements. It is a mutation exceeding the local scope of the loop: UI is indeed a side effect. We just need a proper way to incorporate it in the definition of a Spin.

Once a Spin is built, we can “decorate” it with a new feedback dedicated to the UI rendering/interactions. A special type of Spin exists to perform that decoration: UISpin.

As a global picture, we can illustrate a feedback loop in the context of a UI with this diagram:

Feedback Loop

In a ViewController, let’s say you have a rendering function like:

func render(state: State) {
    switch state {
    case .increasing(let value):
        self.counterLabel.text = "\(value)"
        self.counterLabel.textColor = .green
    case .decreasing(let value):
        self.counterLabel.text = "\(value)"
        self.counterLabel.textColor = .red
    }
}

We need to decorate the “business” Spin with a UISpin instance variable of the ViewController so their lifecycle is bound:

// previously defined or injected: counterSpin is the Spin that handles our counter business
self.uiSpin = UISpin(spin: counterSpin)

// self.uiSpin is now able to handle UI side effects

// we now want to attach the UI Spin to the rendering function of the ViewController:
self.uiSpin.render(on: self, using: { $0.render(state:) })

And once the view is ready (in “viewDidLoad” function for instance) let’s start the loop:

Observable
    .start(spin: self.uiSpin)
    .disposed(by: self.disposeBag)

or a shortest version:

self.uiSpin.start()
// the underlying reactive stream will be disposed once the uiSpin will be deinit

Sending events in the loop is very straightforward; simply use the emit function:

self.uiSpin.emit(Event.startCounter)

Using Spin in a SwiftUI based app

Because SwiftUI relies on the idea of a binding between a State and a View and takes care of the rendering, the way to connect the SwiftUI Spin is slightly different, and even simpler.

In your view you have to annotate the SwiftUI Spin variable with “@ObservedObject” (a SwiftUISpin being an “ObservableObject”):

@ObservedObject
private var uiSpin: SwiftUISpin<State, Event> = {
    // previously defined or injected: counterSpin is the Spin that handles our counter business
    let spin = SwiftUISpin(spin: counterSpin)
    spin.start()
    return spin
}()

you can then use the “uiSpin.state” property inside the view to display data and uiSpin.emit() to send events:

Button(action: {
    self.uiSpin.emit(Event.startCounter)
}) {
    Text("\(self.uiSpin.state.isCounterPaused ? "Start": "Stop")")
}

A SwiftUISpin can also be used to produce SwiftUI bindings:

Toggle(isOn: self.uiSpin.binding(for: \.isPaused, event: .toggle) {
    Text("toggle")
}

\.isPaused is a keypath which designates a sub state of the state, and .toggle is the event to emit when the toggle is changed.

Using Spin with multiple Reactive Frameworks

As stated in the introduction, Spin aims to ease the cohabitation between several reactive frameworks inside your apps to allow a smoother transition. As a result, you may have to differentiate a RxSwift Feedback from a Combine Feedback since they share the same type name, which is Feedback. The same goes for Reducer, Spin, UISpin and SwiftUISpin.

The Spin frameworks (SpinRxSwift, SpinReactiveSwift and SpinCombine) come with typealiases to differentiate their inner types.

For instance RxFeedback is a typealias for SpinRxSwift.Feedback, CombineFeedback is the one for SpinCombine.Feedback.

By using those typealiases, it is safe to use all the Spin flavors inside the same source file.

All the Demo applications use the three reactive frameworks at the same time. But the advanced demo application is the most interesting one since it uses those frameworks in the same source files (for dependency injection) and take advantage of the provided typealiases.

How to make spins talk together

There are some use cases where two (or more) feedback loops have to talk together directly, without involving existing side effects (like the UI for instance).

A typical use case would be when you have a feedback loop that handles the routing of your application and checks for the user's authentication state when the app starts. If the user is authorized then the home screen is presented, otherwise a login screen is presented. Assuredly, once authorized, the user will use features that fetch data from a backend and as a consequence that can lead to authorization issues. In that case you'd like the loops that drive those features to communicate with the routing one in order to trigger a new authorization state checking.

In design patterns, this kind of need is fulfilled thanks to a Mediator. This is a transversale object used as a communication bus between independent systems.

In Spin, the mediator equivalent is called a Gear. A Gear can be attached to several feedbacks, allowing them to push and receive events.

Gear

How to attach Feedbacks to a Gear so it can push/receive events from it ?

First thing first, a Gear must be created:

// A Gear has its own event type:
enum GearEvent {
    case authorizationIssueHappened
}

let gear = Gear<GearEvent>()

We have to tell a feedback from the check authorization Spin how to react to events happening in the Gear:

let feedback = Feedback<State, Event>(attachedTo: gear, propagating: { (event: GearEvent) in
	if event == .authorizationIssueHappened {
		// the feedback will emit an event only in case of .authorizationIssueHappened
		return .checkAuthorization
	}
	return nil
})

// or with the short syntax

let feedback = Feedback<State, Event>(attachedTo: gear, catching: .authorizationIssueHappened, emitting: .checkAuthorization)

...
// then, create the Check Authorization Spin with this feedback
...

At last we have to tell a feedback from the feature Spin how it will push events in the Gear:

let feedback = Feedback<State, Event>(attachedTo: gear, propagating: { (state: State) in
	if state == .unauthorized {
		// only the .unauthorized state should trigger en event in the Gear
		return .authorizationIssueHappened
	}
	return nil
})

// or with the short syntax

let feedback = Feedback<State, Event>(attachedTo: gear, catching: .unauthorized, propagating: .authorizationIssueHappened)

...
// then, create the Feature Spin with this feedback
...

This is what will happen when the feature spin is in state .unauthorized:

FeatureSpin: state = .unauthorized

Gear: propagate event = .authorizationIssueHappened

AuthorizationSpin: event = .checkAuthorization

AuthorizationSpin: state = authorized/unauthorized

Of course in this case, the Gear must be shared between the two Spins. You might have to make it a Singleton depending on your use case.

Demo applications

In the Spinners organization, you can find 2 demo applications demonstrating the usage of Spin with RxSwift, ReactiveSwift, and Combine.

Installation

Swift Package Manager

Add this URL to your dependencies:

https://github.com/Spinners/Spin.Swift.git

Carthage

Add the following entry to your Cartfile:

github "Spinners/Spin.Swift" ~> 0.20.0

and then:

carthage update Spin.Swift

CocoaPods

Add the following dependencies to your Podfile:

pod 'SpinReactiveSwift', '~> 0.20.0'
pod 'SpinCombine', '~> 0.20.0'
pod 'SpinRxSwift', '~> 0.20.0'

You should then be able to import SpinCommon (base implementation), SpinRxSwift, SpinReactiveSwift or SpinCombine

Acknowledgements

The advanced demo applications use Alamofire for their network stack, Swinject for dependency injection, Reusable for view instantiation (UIKit version) and RxFlow for the coordinator pattern (UIKit version).

The following repos were also a source of inspiration:

You might also like...
Viper Framework for iOS using Swift
Viper Framework for iOS using Swift

Write an iOS app following VIPER architecture. But in an easy way. Viper the easy way We all know Viper is cool. But we also know that it's hard to se

Swift Interaction with VIPER Architecture
Swift Interaction with VIPER Architecture

SwiftyVIPER SwiftyVIPER allows easy use of VIPER architecture throughout your iOS application. VIPER Architecture What is VIPER? Great question! VIPER

Sample applications of iOS Design patterns written using swift.

ios-design-patterns This repo contains all my Sample applications of iOS Design patterns written using swift. Link for my Design patterns Blog : https

IOS Spin Game - A simple spin game using SwiftUI
IOS Spin Game - A simple spin game using SwiftUI

IOS_Spin_Game A simple spin game using Swift UI.

 Reactive extensions to Cocoa frameworks, built on top of ReactiveSwift.
Reactive extensions to Cocoa frameworks, built on top of ReactiveSwift.

ReactiveSwift offers composable, declarative and flexible primitives that are built around the grand concept of streams of values over time. These primitives can be used to uniformly represent common Cocoa and generic programming patterns that are fundamentally an act of observation.

SkyWite is an open-source and highly versatile multi-purpose frameworks.

SkyWite is an open-source and highly versatile multi-purpose frameworks. Clean code and sleek features make SkyWite an ideal choice. Powerful high-level networking abstractions built into Cocoa. It has a modular architecture with well-designed, feature-rich APIs that are a joy to use.

AuroraEditor is a IDE built by the community, for the community, and written in Swift for the best native performance and feel for macOS.
AuroraEditor is a IDE built by the community, for the community, and written in Swift for the best native performance and feel for macOS.

AuroraEditor AuroraEditor is a IDE built by the community, for the community, and written in Swift for the best native performance and feel for macOS.

Realm RxSwift - This application was written in order to use Realm, RxSwift frameworks in real example

Realm_RxSwift This simple app was written to introduce basic operations of some

Reflection based (Dictionary, CKRecord, NSManagedObject, Realm, JSON and XML) object mapping with extensions for Alamofire and Moya with RxSwift or ReactiveSwift

EVReflection General information At this moment the master branch is tested with Swift 4.2 and 5.0 beta If you want to continue using EVReflection in

Reflection based (Dictionary, CKRecord, NSManagedObject, Realm, JSON and XML) object mapping with extensions for Alamofire and Moya with RxSwift or ReactiveSwift

EVReflection General information At this moment the master branch is tested with Swift 4.2 and 5.0 beta If you want to continue using EVReflection in

Swift programming language hackathon. Implementation of the main logic of working with an ATM in the Playground environment.
Swift programming language hackathon. Implementation of the main logic of working with an ATM in the Playground environment.

Hackaton-ATM-PJ04 Swift programming language hackathon. Implementation of the main logic of working with an ATM in the Playground environment. The tas

WebKit aims to provide platform agnostic isolated browser environments without the need for sketchy C bindings or a bloated V8 runtime.

WebKit WebKit aims to provide platform agnostic isolated browser environments without the need for sketchy C bindings or a bloated V8 runtime. Running

Async State Machine aims to provide a way to structure an application thanks to state machines
Async State Machine aims to provide a way to structure an application thanks to state machines

Async State Machine Async State Machine aims to provide a way to structure an application thanks to state machines. The goal is to identify the states

Versatile Video Player implementation for iOS, macOS, and tvOS
Versatile Video Player implementation for iOS, macOS, and tvOS

News 🎉 - Since 2.1.3 VersaPlayer now supports iOS, macOS, and tvOS Example Installation Usage Basic Usage Adding Controls Advanced Usage Encrypted Co

SwiftUI Animation Library. Useful SwiftUI animations including Loading/progress, Looping, On-off, Enter, Exit, Fade, Spin and Background animations that you can directly implement in your next iOS application or project. The library also contains huge examples of spring animations such as Inertial Bounce, Shake, Twirl, Jelly, Jiggle, Rubber Band, Kitchen Sink and Wobble effects. Browse, find and download the animation that fits your needs. Whisper is a component that will make the task of display messages and in-app notifications simple. It has three different views inside
Whisper is a component that will make the task of display messages and in-app notifications simple. It has three different views inside

Description 🍃 Break the silence of your UI, whispering, shouting or whistling at it. Whisper is a component that will make the task of displaying mes

Versatile HTTP Networking in Swift
Versatile HTTP Networking in Swift

Net is a versatile HTTP networking library written in Swift. 🌟 Features URL / JSON / Property List Parameter Encoding Upload File / Data / Stream / M

Reactive WebSockets - A lightweight abstraction layer over Starscream to make it reactive.

RxWebSocket Reactive extensions for websockets. A lightweight abstraction layer over Starscream to make it reactive. Installation RxWebSocket is avail

JDVideoKit - You can easily transfer your video into Three common video type.
JDVideoKit - You can easily transfer your video into Three common video type.

JDVideoKit Introduction You can easily transfer your video into Three common video type. You can use set up camera easily. Installation pod 'JDVideoK

Comments
  • Feature/group implementation in package

    Feature/group implementation in package

    This PR aims to group all the Spin packages into a single one, and distributing the framework through multiple libraries inside a unified swift package

    opened by twittemb 0
Releases(0.21.0)
  • 0.21.0(Sep 27, 2020)

  • 0.20.0(Aug 24, 2020)

  • 0.19.0(Aug 10, 2020)

  • 0.18.0(Aug 9, 2020)

    This release refactors the scheduling of the loops. While it is still possible to specify a scheduler for each feedback, the global scheduling of the loop is now declared directly at the Spin definition and no more at the Reducer level.

    If no executer is specified, default schedulers are applied to each loop (on background serial queues).

    Source code(tar.gz)
    Source code(zip)
  • 0.17.0(Jul 27, 2020)

  • 0.16.1(Apr 26, 2020)

  • 0.16.0(Apr 13, 2020)

  • 0.15.0(Mar 15, 2020)

    This release renames types to erase the specificities of each reactive frameworks:

    • RxFeedback, ReactiveFeedback, CombineFeedback -> Feedback
    • RxReducer, ReactiveReducer, CombineReducer -> Reducer
    • RxSpin, ReactiveSpin, CombineSpin -> Spin
    • RxUISpin, ReactiveUISpin, CombineUISpin -> UISpin
    • RxSwiftUISpin, ReactiveSwiftUISpin, CombineSwiftUISpin -> SwiftUISpin

    If you have to mix several reactive frameworks in the same app, you can use the provided type aliases (RxFeedback for Spin_RxSwift.Feedback, ReactiveFeedback for Spin_ReactiveSwift.Feedback, ...)

    Source code(tar.gz)
    Source code(zip)
  • 0.14.0(Mar 10, 2020)

    This release adds a start() function to xxUISpin and xxSwiftUISpin. This allows the developper to not handle the subscription by himself.

    Source code(tar.gz)
    Source code(zip)
  • 0.13.0(Mar 10, 2020)

    This release:

    • brings some refactoring in ReactiveStream to remove unused code and bring the 'consume' function
    • factorizes some code between xxUISpin and xxSwiftUISpin
    • removes the subscription from the xxUISpin and xxSwiftUISpin in order to let the user handle it
    Source code(tar.gz)
    Source code(zip)
  • 0.12.0(Mar 7, 2020)

  • 0.11.0(Mar 6, 2020)

    This release:

    • renames ExecutionStrategy cases
    • renames Spinner functions/parameters to be less verbose
    • improves type inference for the stream operators
    • shorten the reducer signature
    • add examples in the readme
    Source code(tar.gz)
    Source code(zip)
  • 0.10.0(Mar 4, 2020)

    The release lowers the minimum iOS/macOS/tvOS/watchOS versions so that the RxSwift and ReactiveSwift versions of feedback loops are usable for a large majority of projects.

    The Combine version is available starting at iOS13, macOS 10.15, tvOS 13.0 and watchOS 6.0

    Source code(tar.gz)
    Source code(zip)
  • 0.9.0(Feb 25, 2020)

  • 0.8.0(Feb 11, 2020)

    The release brings:

    • the ability to create a feedback that lenses on a substate with a keypath
    • the ability to create a binding in xxUISpins directly emitting an event
    Source code(tar.gz)
    Source code(zip)
  • 0.7.2(Feb 9, 2020)

  • 0.7.1(Feb 9, 2020)

  • 0.7.0(Feb 9, 2020)

    This release:

    • decouples the concrete Reducers implementations from the reactive streams building
    • introduces a custom static functions on Observable, SignalProducer and AnyPublisher to build the reactive stream from an AnySpin
    • replaces the XXXViewContext by XXUISpin in order to ease DI in client applications
    Source code(tar.gz)
    Source code(zip)
  • 0.6.0(Jan 28, 2020)

  • 0.5.0(Jan 24, 2020)

    This release contains:

    • rename Mutation to Event
    • rename Reducer.reduce in Reducer.apply(on:after:)
    • rename StreamState/StreamEvent to StateStream/EventStream
    • in XXViewContext: call the external state rendering function after being set
    • in XXViewContext: rename perform to emit
    • add a Result based filter inn Feedback
    • add a Binding feature in XXViewContext
    • rename feedbackStream to effect
    • add the possibility to init a Feedback with a (State) -> Event effect
    • replace the static 'make' funcs by inits in Feedback
    • Feedback are no more composed of other feedbacks
    Source code(tar.gz)
    Source code(zip)
  • 0.4.1(Jan 10, 2020)

  • 0.4.0(Jan 10, 2020)

  • 0.3.1(Jan 9, 2020)

  • 0.3.0(Jan 9, 2020)

    This release brings the concept of ViewContext.

    A ViewContext is the best way to represent a UI Feedback, especially in a SwiftUI context as it is also an ObservableObject.

    Source code(tar.gz)
    Source code(zip)
  • 0.2.0(Jan 1, 2020)

  • 0.1.0(Dec 31, 2019)

    Initial version of the Spin framework. It provides an abstraction and a default implementation for Feedback Loop. The three main reactive frameworks in the Swift community will support Spin.

    Source code(tar.gz)
    Source code(zip)
Owner
Spinners
Implementation of a unified syntax feedback-loop system, compatible with RxSwift, ReactiveSwift and Combine
Spinners
Reactant is a reactive architecture for iOS

Reactant Reactant is a foundation for rapid and safe iOS development. It allows you to cut down your development costs by improving reusability, testa

Brightify 374 Nov 22, 2022
A collection of iOS architectures - MVC, MVVM, MVVM+RxSwift, VIPER, RIBs and many others

ios-architecture WIP ?? ?? ?? ??️ Demystifying MVC, MVVM, VIPER, RIBs and many others A collection of simple one screen apps to showcase and discuss d

Pawel Krawiec 1.3k Jan 3, 2023
SwiftUI sample app using Clean Architecture. Examples of working with CoreData persistence, networking, dependency injection, unit testing, and more.

Articles related to this project Clean Architecture for SwiftUI Programmatic navigation in SwiftUI project Separation of Concerns in Software Design C

Alexey Naumov 4k Dec 31, 2022
Example of Clean Architecture of iOS app using RxSwift

Clean architecture with RxSwift Contributions are welcome and highly appreciated!! You can do this by: opening an issue to discuss the current solutio

null 3.6k Dec 29, 2022
MoneySafe - Application for tracking income and expenses and analyzing expenses. VIPER architecture, CoreData, Charts

?? MoneySafe ?? Application for tracking income and expenses and analyzing expen

Vladislav 5 Dec 27, 2022
A Swift 4.2 VIPER Module Boilerplate Generator with predefined functions and a BaseViewProtocol.

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away. Are you new to VIPER Design Pattern? Want

Mohamad Kaakati 68 Sep 29, 2022
This is an example of clean architecture and MVVM pattern written in swift

Swift Clean Architecture MVVM This is an example of clean architecture and MVVM pattern written in swift First of all thanks to all of those who made

null 19 Oct 12, 2022
Stateful view controller containment for iOS and tvOS

StateViewController When creating rich view controllers, a single view controller class is often tasked with managing the appearance of many other vie

David Ask 309 Nov 29, 2022
A holistic approach to iOS development, inspired by Redux and MVVM

Tempura is a holistic approach to iOS development, it borrows concepts from Redux (through Katana) and MVVM. ?? Installation Requirements CocoaPods Sw

Bending Spoons 693 Jan 4, 2023
📖 Design Patterns implemented in Swift 5.0

Design Patterns implemented in Swift 5.0 A short cheat-sheet with Xcode 10.2 Playground (Design-Patterns.playground.zip). ???? 中文版 ?? Project started

Oktawian Chojnacki 13.9k Dec 31, 2022