Skip to content

Commit

Permalink
readme: add a Gear section
Browse files Browse the repository at this point in the history
  • Loading branch information
twittemb committed Jul 26, 2020
1 parent 15389ad commit aef71f2
Show file tree
Hide file tree
Showing 14 changed files with 97 additions and 19 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
**version 0.17.0**:

* introduce Gear: a mediatior pattern between spin that allows them to communicate together
* introduce Gear: a mediator pattern between Spins that allows them to communicate together
* include the Reducer in the Spin definition with the DSL like syntax

**version 0.16.1**:
Expand Down
82 changes: 80 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- <a href="#using-spin-in-a-uikit-or-appkit-based-app">Using Spin in a UIKit or AppKit based app</a>
- <a href="#using-spin-in-a-swiftUI-based-app">Using Spin in a SwiftUI based app</a>
- <a href="#using-spin-with-multiple-reactive-frameworks">Using Spin with multiple Reactive Frameworks</a>
- <a href="#how-to-make-spins-talk-together">How to make spins talk together</a>
- <a href="#demo-applications">Demo applications</a>
- <a href="#installation">Installation</a>
- <a href="#acknowledgements">Acknowledgements</a>
Expand Down Expand Up @@ -378,14 +379,91 @@ Toggle(isOn: self.uiSpin.binding(for: \.isPaused, event: .toggle) {

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 (Spin_RxSwift, Spin_ReactiveSwift and Spin_Combine) come with typealiases to differentiate their inner types.
The Spin frameworks (SpinRxSwift, SpinReactiveSwift and SpinCombine) come with typealiases to differentiate their inner types.

For instance `RxFeedback` is a typealias for `Spin_RxSwift.Feedback`, `CombineFeedback` is the one for `Spin_Combine.Feedback`.
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](https://github.com/Spinners/Spin.UIKit.Demo) 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 feedbackloop 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 a authorization issues. In that case you'd like the feedbackloops 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.

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

<img alt="Gear" src="https://raw.githubusercontent.com/Spinners/Spin.Swift/master/Resources/gear.png" border="1"/>

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

First thing first, a Gear must be created:

```swift
// 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:

```swift
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
})

...
// create the Check Authorization Spin with this feedback
...
```

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

```swift
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
})

...
// 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 it this case, the Gear must be shared between 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.
Expand Down
Binary file added Resources/gear.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Sources/Combine/Feedback.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ where SchedulerTime: Strideable, SchedulerTime.Stride: SchedulerTimeIntervalConv
self.init(effect: fullEffect, on: nil)
}

public init<Event>(attachTo gear: Gear<Event>,
public init<Event>(attachedTo gear: Gear<Event>,
propagating block: @escaping (Event) -> EventStream.Value?,
on executer: Executer? = nil) {
let effect: (StateStream) -> EventStream = { _ in
Expand Down
2 changes: 1 addition & 1 deletion Sources/Common/FeedbackDefinition+Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ public extension FeedbackDefinition {
self.init(effect: effectFromKeyPath, filteredBy: filterState, on: executer, applying: strategy)
}

init<GearType: GearDefinition>(attachTo gear: GearType,
init<GearType: GearDefinition>(attachedTo gear: GearType,
propagating block: @escaping (StateStream.Value) -> GearType.Event?,
on executer: Executer? = nil,
applying strategy: ExecutionStrategy = Self.defaultExecutionStrategy) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/ReactiveSwift/Feedback.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public struct Feedback<State, Event>: FeedbackDefinition {
self.init(effect: fullEffect, on: nil)
}

public init<Event>(attachTo gear: Gear<Event>,
public init<Event>(attachedTo gear: Gear<Event>,
propagating block: @escaping (Event) -> EventStream.Value?,
on executer: Executer? = nil) {
let effect: (StateStream) -> EventStream = { _ in
Expand Down
2 changes: 1 addition & 1 deletion Sources/RxSwift/Feedback.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public struct Feedback<State, Event>: FeedbackDefinition {
self.init(effect: fullEffect, on: nil)
}

public init<Event>(attachTo gear: Gear<Event>,
public init<Event>(attachedTo gear: Gear<Event>,
propagating block: @escaping (Event) -> EventStream.Value?,
on executer: Executer? = nil) {
let effect: (StateStream) -> EventStream = { _ in
Expand Down
2 changes: 1 addition & 1 deletion Tests/CombineTests/FeedbackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ final class FeedbackTests: XCTestCase {
var numberOfCallsGearSideEffect = 0

// Given: a feedback attached to a Gear and triggering en event only of the gear event is 1
let sut = Feedback<Int, String>(attachTo: spyGear, propagating: { gearEvent -> String? in
let sut = Feedback<Int, String>(attachedTo: spyGear, propagating: { gearEvent -> String? in
numberOfCallsGearSideEffect += 1
if gearEvent == 1 {
return "event"
Expand Down
4 changes: 2 additions & 2 deletions Tests/CombineTests/SpinIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ private extension SpinIntegrationTests {
return Just<CheckAuthorizationSpinEvent>(.revokeUser).eraseToAnyPublisher()
}

let attachedCheckAuthorizationFeedback = Feedback<CheckAuthorizationSpinState, CheckAuthorizationSpinEvent>(attachTo: gear,
let attachedCheckAuthorizationFeedback = Feedback<CheckAuthorizationSpinState, CheckAuthorizationSpinEvent>(attachedTo: gear,
propagating: { (event: GearEvent) in
if event == .authorizedIssueDetected {
return CheckAuthorizationSpinEvent.checkAuthorization
Expand Down Expand Up @@ -262,7 +262,7 @@ private extension SpinIntegrationTests {
return Just<FetchFeatureSpinEvent>(.userIsNotAuthorized).eraseToAnyPublisher()
}

let attachedFetchFeatureFeedback = Feedback<FetchFeatureSpinState, FetchFeatureSpinEvent>(attachTo: gear,
let attachedFetchFeatureFeedback = Feedback<FetchFeatureSpinState, FetchFeatureSpinEvent>(attachedTo: gear,
propagating: { (state: FetchFeatureSpinState) in
if state == .unauthorized {
return GearEvent.authorizedIssueDetected
Expand Down
4 changes: 2 additions & 2 deletions Tests/CommonTests/FeedbackDefinition+DefaultTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ final class FeedbackDefinition_DefaultTests: XCTestCase {
let spyGear = MockGear()

// Given: a feedback built with a gear attachment that return a nil MockGearEVent
let sut = SpyFeedback<MockState, MockEvent>(attachTo: spyGear, propagating: { state -> MockGearEvent? in
let sut = SpyFeedback<MockState, MockEvent>(attachedTo: spyGear, propagating: { state -> MockGearEvent? in
return nil
})

Expand All @@ -381,7 +381,7 @@ final class FeedbackDefinition_DefaultTests: XCTestCase {
let expectedMockGearEvent = MockGearEvent.event

// Given: a feedback built with a gear attachment
let sut = SpyFeedback<MockState, MockEvent>(attachTo: spyGear, propagating: { state -> MockGearEvent? in
let sut = SpyFeedback<MockState, MockEvent>(attachedTo: spyGear, propagating: { state -> MockGearEvent? in
return expectedMockGearEvent
})

Expand Down
2 changes: 1 addition & 1 deletion Tests/ReactiveSwiftTests/FeedbackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ final class FeedbackTests: XCTestCase {
var receivedEvents = [String]()

// Given: a feedback attached to a Gear and triggering en event only of the gear event is 1
let sut = Feedback<Int, String>(attachTo: spyGear, propagating: { gearEvent -> String? in
let sut = Feedback<Int, String>(attachedTo: spyGear, propagating: { gearEvent -> String? in
numberOfCallsGearSideEffect += 1
if gearEvent == 1 {
return "event"
Expand Down
4 changes: 2 additions & 2 deletions Tests/ReactiveSwiftTests/SpinIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ private extension SpinIntegrationTests {
return SignalProducer<CheckAuthorizationSpinEvent, Never>(value: .revokeUser)
}

let attachedCheckAuthorizationFeedback = Feedback<CheckAuthorizationSpinState, CheckAuthorizationSpinEvent>(attachTo: gear,
let attachedCheckAuthorizationFeedback = Feedback<CheckAuthorizationSpinState, CheckAuthorizationSpinEvent>(attachedTo: gear,
propagating: { (event: GearEvent) in
if event == .authorizedIssueDetected {
return CheckAuthorizationSpinEvent.checkAuthorization
Expand Down Expand Up @@ -276,7 +276,7 @@ private extension SpinIntegrationTests {
return SignalProducer<FetchFeatureSpinEvent, Never>(value: .userIsNotAuthorized)
}

let attachedFetchFeatureFeedback = Feedback<FetchFeatureSpinState, FetchFeatureSpinEvent>(attachTo: gear,
let attachedFetchFeatureFeedback = Feedback<FetchFeatureSpinState, FetchFeatureSpinEvent>(attachedTo: gear,
propagating: { (state: FetchFeatureSpinState) in
if state == .unauthorized {
return GearEvent.authorizedIssueDetected
Expand Down
2 changes: 1 addition & 1 deletion Tests/RxSwiftTests/FeedbackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ final class FeedbackTests: XCTestCase {
var receivedEvents = [String]()

// Given: a feedback attached to a Gear and triggering en event only of the gear event is 1
let sut = Feedback<Int, String>(attachTo: spyGear, propagating: { gearEvent -> String? in
let sut = Feedback<Int, String>(attachedTo: spyGear, propagating: { gearEvent -> String? in
numberOfCallsGearSideEffect += 1
if gearEvent == 1 {
return "event"
Expand Down
6 changes: 3 additions & 3 deletions Tests/RxSwiftTests/SpinIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ private extension SpinIntegrationTests {
return Observable<CheckAuthorizationSpinEvent>.just(.revokeUser)
}

let attachedCheckAuthorizationFeedback = Feedback<CheckAuthorizationSpinState, CheckAuthorizationSpinEvent>(attachTo: gear,
let attachedCheckAuthorizationFeedback = Feedback<CheckAuthorizationSpinState, CheckAuthorizationSpinEvent>(attachedTo: gear,
propagating: { (event: GearEvent) in
if event == .authorizedIssueDetected {
return CheckAuthorizationSpinEvent.checkAuthorization
Expand Down Expand Up @@ -260,8 +260,8 @@ private extension SpinIntegrationTests {
return Observable<FetchFeatureSpinEvent>.just(.userIsNotAuthorized)
}

let attachedFetchFeatureFeedback = Feedback<FetchFeatureSpinState, FetchFeatureSpinEvent>(attachTo: gear,
propagating: { (state: FetchFeatureSpinState) in
let attachedFetchFeatureFeedback = Feedback<FetchFeatureSpinState, FetchFeatureSpinEvent>(attachedTo: gear,
propagating: { (state: FetchFeatureSpinState) -> GearEvent? in
if state == .unauthorized {
return GearEvent.authorizedIssueDetected
}
Expand Down

0 comments on commit aef71f2

Please sign in to comment.