How to subscribe to multiple states separately in one class
ReSwift Version: 4.0.0
Problem: I have to listen to multiple sub-states separately in one class
Currently, i am doing it like:
class Sample: StoreSubscriber {
init() {
// MARK: Subscriber of state 1, 2
appStore.subscribe(self) { state in
state.select { state in (state.state1, state.state2) }
}
}
// MARK: Listener for state 1, 2
func newState(state: (state1: State1, state2: State2) ) {
// process
}
}
How can i listen to each sub-state separately, so that i can act on only sub-state which get changed??
SomethingLike:
class Sample: StoreSubscriber {
init() {
// MARK: Subscriber
appStore.subscribe(self) { state in
state.select { state in state.state1 }
}
appStore.subscribe(self) { state in
state.select { state in state.state2 }
}
}
// MARK: Listener for state1
func newState(state: State1 ) {
// process
}
// MARK: Listener for state2
func newState(state: State2 ) {
// process
}
}
I've run into this. I created "watcher" classes that focus on a particular part of the state tree. These classes can then be instantiated by the mother class, as you have listed here. When a state change fires, the "watcher" class calls into the mother class via a delegate method.
Mark
Mark S Broski
From: gkmrakesh [email protected] Sent: Friday, January 12, 2018 11:14:11 AM To: ReSwift/ReSwift Cc: Subscribed Subject: [ReSwift/ReSwift] How to subscribe to multiple states separately in one class (#318)
ReSwift Version: 4.0.0
Problem: I have to listen to multiple sub-states separately in one class
Currently, i am doing it like:
class Sample: StoreSubscriber { init() { // MARK: Subscriber of state 1, 2 appStore.subscribe(self) { state in state.select { state in (state.state1, state.state2) } } }
// MARK: Listener for state 1, 2 func newState(state: (state1: State1, state2: State2) ) { // process }
}
How can i listen to each sub-state separately, so that i can act on only sub-state which get changed??
SomethingLike:
class Sample: StoreSubscriber { init() { // MARK: Subscriber appStore.subscribe(self) { state in state.select { state in state.state1 } }
appStore.subscribe(self) { state in
state.select { state in state.state2 }
}
}
// MARK: Listener for state1 func newState(state: State1 ) { // process }
// MARK: Listener for state2 func newState(state: State2 ) { // process }
}
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHubhttps://github.com/ReSwift/ReSwift/issues/318, or mute the threadhttps://github.com/notifications/unsubscribe-auth/AB1EpXtVPtto8-M8idxqtflY2Gcpqamfks5tJ4TTgaJpZM4Rcgmx.
You can't; you'd need to write 2 subscribers and 1 combined state change handler as separate objects.
That being said, I wonder if your state is partitioned well if you run into this. Can you give a more concrete example?
In order to subscribe to multiple states you'll need to use some sort of helper object.
You could do this with a Reactive helper (such as ReRxSwift), or helper objects
Here is a block-based subscriber example:
public class BlockSubscriber<S>: StoreSubscriber {
public typealias StoreSubscriberStateType = S
private let block: (S) -> Void
public init(block: @escaping (S) -> Void) {
self.block = block
}
public func newState(state: S) {
self.block(state)
}
}
Then in your class, you can:
class Sample {
private lazy var state1Subscriber: BlockSubscriber<State1> = BlockSubscriber(block: { [unowned self], state1 in
self.something = state1
})
private lazy var state2Subscriber: BlockSubscriber<State2> = BlockSubscriber(block: { [unowned self], state2 in
self.something2 = state2
})
init() {
// MARK: Subscriber
appStore.subscribe(self.state1Subscriber) { state in
state.select { state in state.state1 }
}
appStore.subscribe(self.state2Subscriber) { state in
state.select { state in state.state2 }
}
}
}
@mbroski , @DivineDominion ,
Thanks for your quick reply.
@mjarvis ,
Awesome, after months of struggle I can able to separate subscribers.
But, issue remains same,
When my sub-state changes, all subscribers (state1Subscriber, state2Subscriber in below code) getting notified.
This brings me back to initial question. How can I listen to only sub-state which get changed??
For example in below code, if "Counter" state changes then only state1Subscriber should get notified.
import UIKit
import ReSwift
public class BlockSubscriber<S>: StoreSubscriber {
public typealias StoreSubscriberStateType = S
private let block: (S) -> Void
public init(block: @escaping (S) -> Void) {
self.block = block
}
public func newState(state: S) {
self.block(state)
}
}
class ViewController: UIViewController {
@IBOutlet weak var counterLabel: UILabel!
@IBOutlet weak var counterLabel2: UILabel!
private lazy var state1Subscriber: BlockSubscriber<Counter> = BlockSubscriber(block: { [unowned self] state1 in
print(state1)
})
private lazy var state2Subscriber: BlockSubscriber<Counter2> = BlockSubscriber(block: { [unowned self] state2 in
print(state2)
})
override func viewDidLoad() {
super.viewDidLoad()
mainStore.subscribe(self.state1Subscriber) { state in
state.select { state in state.counter }
}
mainStore.subscribe(self.state2Subscriber) { state in
state.select { state in state.counter2 }
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
func newState(state: Counter) {
// when the state changes, the UI is updated to reflect the current state
counterLabel.text = "\(state.data)"
}
func newState(state: Counter2) {
// when the state changes, the UI is updated to reflect the current state
counterLabel.text = "\(state.data)"
}
@IBAction func downTouch(_ sender: AnyObject) {
mainStore.dispatch(CounterActionDecrease());
}
@IBAction func upTouch(_ sender: AnyObject) {
mainStore.dispatch(CounterActionIncrease());
}
@IBAction func downTouch2(_ sender: Any) {
mainStore.dispatch(CounterActionDecrease2());
}
@IBAction func upTouch2(_ sender: Any) {
mainStore.dispatch(CounterActionIncrease2());
}
}
if counter2 is : Equatable and you have automaticallySkipRepeats enabled (the default) then it should skip them automatically. If not you might have to implement skipRepeats yourself:
mainStore.subscribe(self.state2Subscriber) { state in
state.select { state in state.counter2 }
}.skipRepeats(==)
@mjarvis
I have all of my substates : Equatable; however, unless the entire_substate that the StoreSubscriber subscribes to is ==, every substate within entire_substate will show up in state in newState. It would be great if we could subscribe to an entire_substate but only have changed substates show up in newState's state variable
or someway to access oldState in newState would also work
edit: maybe that's what newValues() is for. will try and let you guys know! (re:edit couldn't get this to work)
It would be great if we could subscribe to an entire_substate but only have changed substates show up in newState's state variable or someway to access oldState in newState would also work
That is not going to work. I think if you could explain a little more about your use case and also show us your state, we could help.
One way, is to subscribe to just what you are interested in. Create smaller subscribers that listen to what's changing in the substate alone. But I would say you should design your state and actions such that this is not needed.
For example, one way I handle this is, let's say on a screen you have a button which when clicked opens the camera. You dispatch an action "openCamera" when the button is touched. You have a flag in a substate "CameraState" that is "openCamera: Bool". This flag is set to true when the action openCamera is fired.
Your view subscribes to changes in the substate "CameraState". If the flag openCamera is set to true you then work with the UIImagePickerController to show the camera. Once the camera is opened, you fire another action "cameraOpened" that sets "openCamera: Bool" flag in your state to false. This ensures that, if you receive any further updates to CameraState, you will not attempt to repeatedly show the camera.
Sorry for delayed response,
Thanks all for your valuable inputs and suggestions. I will check and get back on this.
@mjarvis
This solved part of my problem but there will still be a problem where I want to listen to state1 always(i.e only on changes to state1, even if same value get assigned again) and state2 only on new value.
now only state2 values change still state1 get notified.
is there any way to solve this problem?
@mjarvis This is a wonderful way of handling multiple subscriptions. (one might even get the idea of having the same declarative subscription pattern for single subscriptions too, you know, for consistency and clarity).
I think, for the sake of completeness, you should add the unsubscribe part of the code too. People may forget they have to call something other than appStore.unsubscribe(self) for these kind of situations.
@mohpor Subscriptions are weak, so the block subscribers will be unsubscribed when they're released, so as long as one practices good memory management, there is no need to call unsubscribe
This is a great use case candidate for the documentation. Is anybody willing to write 2 paragraphs about it and add it to the docs? :)
@gkmrakesh
I also was troubled to make subscriptions to multiple states. I made own helper to solve this. ReSwift-Consumer
For more, I found a useful code snip. ReSwift+select
@gkmrakesh @mjarvis @DivineDominion Hi all! This is my solution for multiple states subscribe:
protocol HasAccountState {
var accountState: AccountState { get }
}
protocol HasHomeState {
var homeState: HomeState { get }
}
struct AppState: StateType, HasAccountState, HasHomeState {
let accountState: AccountState
let homeState: HomeState
}
class ViewController: UIViewController, StoreSubscriber {
typealias StoreSubscriberStateType = HasAccountState & HasHomeState
override func viewDidLoad() {
super.viewDidLoad()
store.subscribe(self) { $0.select { $0 as StoreSubscriberStateType } }
}
func newState(state: StoreSubscriberStateType) {
let account = state.accountState
let home = state.homeState
}
}
This is what I do as well, although I don't use protocols. I just create a struct representing the state I need in my view, with a constructor that takes the app state. Something like this:
struct MySubState: Equatable {
// stuff derived from my app state
init(state: AppState) {
// I init here the stuff declared above
}
}
in my view:
store.subscribe(self) { $0.select(MySubState.init) }
func newState(state: MySubState) {
// profit!
}
Consider the MySubState's init as a mapper/selector from your app state to your view state. Being MySubState a struct and conforming to Equatable, you can skip repeats for free and make Swift infers the type of the subscription by itself.
This is such a common pattern that we could provide a subscribe method that takes a subscriber implementing the mapping function, which is the only thing that changes, and let the library do all the boilerplate.
@danielmartinprieto I misunderstood how subscription works. Your solution is simple and the best. I think your must add this solution to the Readme. Thanks!
@danielmartinprieto Would you? Otherwise, let's close this and leave it for later reference.
I work with tuples, by the way, but in a similar fashion. I considered extracting the picking from the AppState into a static function that does the transformation, e.g. MySubscriber.state(appState:), but never found I needed that.
@DivineDominion You mean adding the subscribe method with the mapping fn built in or adding the example to the readme?
Of I meant the README addition, if you think it's worth the effort.
I am collecting how people use ReSwift to assemble a curated "cookbook", so if you rather leave this out of the README, I have a section about this in the guide anyway :)
@DivineDominion done!
currently using @mjarvis 's early solution and modifying it my ideal API:
// MyClass.swift (init:)
self.unsubscribeLogin = mainPubSub.subscribe(topic: .loginState) {
[weak self] state in
guard let `self` = self else { return }
self.onStateChange(state)
}
self.unsubscribeRoute = mainPubSub.subscribe(topic: .routeState) {
[weak self] state in
guard let `self` = self else { return }
self.onStateChange(state)
}
// MyClass.swift (deinit:)
unsubscribeLogin()
unsubscribeRoute()
// Privates
private func onStateChange(state: LoginState) { state.isLoggedIn ? print("Y") : print("N") }
private func onStateChange(state: RouteState) { switch (state.currRoute) {...} }
Anyone has suggestions on how to "map" an enumType (eg. TopicType like .loginState) to a generic type?
I was thinking about creating a dict where key is enum and value is a StoreSubscriber subclass with generics.