Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to subscribe to multiple states separately in one class #318

Open
gkmrakesh opened this issue Jan 12, 2018 · 22 comments
Open

How to subscribe to multiple states separately in one class #318

gkmrakesh opened this issue Jan 12, 2018 · 22 comments

Comments

@gkmrakesh
Copy link

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
   }

}
@mbroski
Copy link

mbroski commented Jan 12, 2018 via email

@DivineDominion
Copy link
Contributor

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?

@mjarvis
Copy link
Member

mjarvis commented Jan 12, 2018

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 }
        }
   }
}

@gkmrakesh
Copy link
Author

gkmrakesh commented Jan 12, 2018

@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());
    }

}

@mjarvis
Copy link
Member

mjarvis commented Jan 12, 2018

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(==)

@dav92lee
Copy link

dav92lee commented Jan 13, 2018

@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)

@Sethuraman
Copy link

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.

@gkmrakesh
Copy link
Author

Sorry for delayed response,

Thanks all for your valuable inputs and suggestions. I will check and get back on this.

@gkmrakesh
Copy link
Author

gkmrakesh commented Jan 30, 2018

@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?

@mohpor
Copy link

mohpor commented Apr 24, 2018

@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.

@mjarvis
Copy link
Member

mjarvis commented Apr 24, 2018

@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

@DivineDominion
Copy link
Contributor

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? :)

@brownsoo
Copy link

@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

@Elshad
Copy link

Elshad commented Mar 15, 2019

@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
    }
}

Screen Shot 2019-03-15 at 15 22 15

@dani-mp
Copy link
Contributor

dani-mp commented Mar 15, 2019

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.

@dani-mp
Copy link
Contributor

dani-mp commented Mar 15, 2019

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.

@Elshad
Copy link

Elshad commented Mar 18, 2019

@danielmartinprieto I misunderstood how subscription works. Your solution is simple and the best. I think your must add this solution to the Readme. Thanks!

@DivineDominion
Copy link
Contributor

DivineDominion commented Mar 21, 2019

@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.

@dani-mp
Copy link
Contributor

dani-mp commented Mar 21, 2019

@DivineDominion You mean adding the subscribe method with the mapping fn built in or adding the example to the readme?

@DivineDominion
Copy link
Contributor

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 :)

@dani-mp
Copy link
Contributor

dani-mp commented Mar 29, 2019

@DivineDominion done!

@Maxlufs
Copy link

Maxlufs commented Oct 14, 2019

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests