google / promises

Promises is a modern framework that provides a synchronization construct for Swift and Objective-C.
Apache License 2.0
3.8k stars 293 forks source link

Is it possible to have multiple observers for a single promise? #34

Closed richardtop closed 6 years ago

richardtop commented 6 years ago

I use Promises in the data-driven iOS app to represent API endpoints.

An app has a case, when two independent views have to be updated based on a single API call. One of the views has a UIRefreshControl, the other one is passive, i.e. it cannot initiate a network call.

Is it possible to subscribe both views to the promise, while being able to initiate a network call only from one of them?

Here is the schema of what I'm trying to achieve: image

Conceptually, it's very similar to hot/cold signals in Reactive approach. However, I wouldn't like to add an extra dependency to the app if it's used only at one place.

ghost commented 6 years ago

Hey Richard, Sorry for the delayed response.

You can definitely have multiple subscribers for a single promise, so if you pass the promise to the "passive" view, that view can chain on the promise to determine when the network call has completed and then update accordingly. However, the main goal of promises is to replace completion handlers, so their intent is simple in that they can only be resolved once and cannot provide a new value in the future. Passing around a promise (or completion handler) to multiple views may not be ideal, in which case, it might be better to broadcast the update to multiple views using notifications (i.e default NSNotification or some sort of custom notification). The view that makes the network call can create a new promise that broadcasts the notification once the network call completes.

Hoping that helps answer your question, but please let me know if I am misunderstanding anything.

Thank you!

richardtop commented 6 years ago

Hi @temrich,

Thanks for your reply. It seems, that I should go for RxSwift and Observable for this task and not reinvent the wheel. Currently I'm using Promises as a substitute for completion handlers in the network calls and passing a promise around doesn't sounds like a good idea.

So, I agree, I'd go with a different solution for propagating the result to multiple subscribers.

shoumikhin commented 6 years ago

Hi Richard,

Just 2c. It’s hard to make any conclusions without seeing the code, but from what you’ve described there may be a chance you’re gonna face some design issues in future.

First, I suspect under the two “views”, one of which is using the service object and the other just passively observes, you really meant view controllers or some other sort of non-view components? Because UIViews shall not own any models or other business logic parts (like networking in you case) and interact with such directly, according to MVC and more advanced successors. So I assume you have two view controllers which are currently both on screen and one of them owns a view with a pull-to-refresh control, which triggers a callback where you initiate a network call, and the other view controller must also be updated when the network call completes.

Second, you’re saying the two views (or rather view controllers, as we clarified) are independent. I guess it’s not quite possible, because those views presumably belong to the same UIWindow (its root view controller), or rather some other parent view which is currently presented. So they both belong to a common view controller at some point.

Now, I believe the network call should be invoked by the controller (or another piece of business logic you may have, like navigation manager or something, depending on which design pattern you’ve adopted) which owns (or, al least, indirectly has references to) all the views you want to update. Thus, the controller which owns the pull-to-refresh control and gets notified when the latter is triggered by the user, can then pass that event to the component which owns both of your views that need to be updated, and also has a reference to the service object and will execute the network call, and once that is completed (in ‘then’ block, if you used Promises), it will tell all views/controllers to update with the new data.

Anyhow, that’s what I cal tell with the limited details provided. Just keep in mind the situation you’ve described seems pretty unusual, people rarely face, and I doubt RxSwift signals would address it well without significant architecture changes.

Would be glad to learn more about your progress and what you eventually come up with.

richardtop commented 6 years ago

Hi Anthony,

Thanks for the explanation and suggestions. In fact, the diagram I posted here is quite simple and represents only a single edge case.

I'll describe the overall app structure, as well as the task I'm trying to solve.

Sorry for being unclear, by "Views" I meant "ViewControllers". I'm designing the app architecture similar to the one described in Advanced User Interfaces with Collection Views, Sample Code. I use a generic UIViewController subclass with a UICollectionView and custom layout. The behavior of the Controller fully depends on the DataSource object. Hence, I referred to the ViewController simply as a View. The DataSource uses the Service to make network calls.

I've attached a more detailed schema of the app, so the challenge could be seen in the context:

group

The app has a global NumberSelector, which is represented as a button in a custom UINavigationBar. To Synchronize multiple UINavigationControllers with currently selected number, I use a separate NumberSelectorState object. It is referenced by all the root UINavigationControllers (since they need to be able to change the number).

NumberSelector is a model object which uses the Observer pattern to propagate changes to all of its subscribers.

The subscribers are all the root UINavigationControllers and all the DataSources which fetch their data from the network.

DataSources might react on number change either by showing a different data (if it is already available), or by triggering a network call, if the data for the new number needs to be fetched.

Some endpoints, i.e. Endpoint1 contains the information for all the numbers, i.e.:

func getAllPhoneNumbers() -> Promise<[PhoneNumber]>

The other ones need to fetch the data from the network, they look like this:

func getDetailsForPhoneNumber(number: PhoneNumber) -> Promise<PhoneNumberDetailsResponse>

Currently, I see 2 options of syncing data from one network endpoint, e.g. Endpoint1 across multiple controllers:

  1. Make a common ancestor handle a network call and update the interested parties (this could be a controller or a service) - the option you proposed.
  2. Use Rx and subscribe all the relevant parties. If any of the parties triggers the update, all the others will be updated passively.

The only problem with approach 1 I see is that with the current app structure, the common ancestor is quite far away from the two controllers/DataSources. It will make the app quite inflexible, as there would be a lot of shared / non relevant code in a single entity.

Having said that, I'd like to hear your opinion/advice on how to link all the modules together, while still having the flexibility.

The approach described in Advanced User Interfaces with Collection Views is quite flexible, but as you can see, there are many ways to synchronize the DataSources.

Thanks in advance!

shoumikhin commented 6 years ago

Many thanks for the details, Richard!

From the first look, sounds like you have (or may need to have) a custom base navigation controller with a custom navigation bar with that button displaying the currently selected number. All other navigation controllers, which need that button to be presented, should inherit from such a base controller. The base navigation controller also subsribes to number-has-changed NSNotification and updates the button accordingly. Then, whenever the number changes due to a network call or somehow else, you also post a notification to inform all interested navigation controllers or other interested parties about the change. Thus, you get rid of the NumberSelectorState object and all the accompanying logic altogether.

Let me know if I’m missing any key details or need to clarify anything.

BTW, you may like to take a look at https://medium.com/square-corner-blog/caviar-ios-migrating-from-advancedcollectionview-to-pjfdatasource-81f8c2e4fcdf and https://github.com/Instagram/IGListKit, as alternatives to Apple’s approach you’ve been following.

richardtop commented 6 years ago

Hi, Anthony,

Thanks for reviewing my app architecture, to keep it structured, I'll reply with a numbered list:

  1. I already use the Custom Navigation Controller approach. However, I don't extend beyond the "CustomNavigationController" class, it's enough.

  2. I've already considered the NSNotification-based approach and decided not to go with it as it promoted implicit binding. However, I want to re-evaluate it, as virtually every DataSource in the app has to react on the number change and refresh the data in one way (a network call) or another (accessing a different local model).

  3. I wouldn't drop NumberSelectorState for now, just change the way it communicates changes (move to NSNotifications) - did you have some other approach in mind? The "State" becomes simply a model which emits notifications.

  4. Thanks for both suggestions, the Caviar article and IGListKit: 4.1 Actually, I've read the Caviar article just before posting here. However, the landscape has changed significantly - now, UICollectionView supports AutoSizing cells (with a slight performance hit). Also, my app is different - it uses UICollectionView everywhere, so it's not an overkill like for the Caviar app. The other difference is that I'm not incorporating the Apple's sample code to my app, but rather using their approach and building my own micro-framework with UI components, which are reused across the whole app. 4.2 I'm well aware of IGListKit, I might go with it if Apple's approach proves to be inflexible.

To sum up:

  1. I'll move to NSNotifications to propagate number changes
  2. So far, Apple's architecture approach works great, except this case. My app is very similar to the iTunes connect and what I've posted is the most complicated part of it.

I'll post some updates here as the app progresses.

shoumikhin commented 6 years ago

Yeah, I guess emitting an NSNotification would be the clearest way to update you singleton object. Let's close the issue for now, since there's probably not much of what any further Promises improvements could help you with at the moment. You're welcome to follow up, and we're happy to hear any feedback.

richardtop commented 6 years ago

@shoumikhin OK, thanks a lot for the feedback and suggestions!

https://www.swiftbysundell.com/posts/observers-in-swift-part-1

This one is an interesting overview on the topic we discussed here. Might be relevant for the people who come to this issue later.