I'm using PromiseKit to simply my API requests.
In this scenario, I'm fetching a list of objects IDs from the server. I need to then fetch details for each ID, and return an array of details. Fairly common scenario.
Effectively, I need to add promises to the promise chain from within a FOR loop which is contained in the FIRST promise.
I've created code that begins to drift right, but the chain completes before the second chain of promises (fill shallow model requests) can be executed.
[PMKPromise promiseWithResolver:^(PMKResolver resolve) {
// Fetch an array of object IDs (shallow objects)
[APIManager fetchObjectListWithCompletion:^(NSArray *resultObjects, NSError *error) {
resolve(error ?: resultObjects[0]);
}];
}].then(^(NSArray *objects){
// Fill each shallow object (query details)
PMKPromise *fetches = [PMKPromise promiseWithValue:nil];
for(id model in objects) {
fetches.then(^{
[APIManager fillShallowObject:model withCompletion:^(NSArray *resultObjects, NSError *error) {
// resolve?
}];
});
}
// Return promise that contains all fill requests
return fetches;
})].then(^{
// This should be executed after all fill requests complete
// Instead it's executed after the initial ID array request
});
Is there a better way to do what I'm trying to accomplish? Perhaps a way to append a promise (.then) with a resolver?
I think you want when:
[AnyPromise promiseWithAdapterBlock:^(id adapter) {
[APIManager fetchObjectListWithCompletion:adapter];
}].then(^(id objects){
NSMutableArray *promises = [NSMutableArray new];
for (id model in objects) {
id promise = [AnyPromise promiseWithAdapterBlock:^(id adapter){
[APIManager fillShallowObject:model withCompletion:adapter];
}];
[promises addObject:promise];
}
return PMKWhen(promises);
}).then(^(NSArray *results){
// when waits on all promises
});
Code is PromiseKit 3.
Related
Ok, I have a signal that sends an event when a certain method of a protocol gets called in response to some data getting retrieved from the server:
self.dataReceivedSignal = [[self rac_signalForSelector:#selector(didReceiveData:) fromProtocol:#protocol(DataServiceDelegate)] mapReplace:#YES];
This signal is then used to fire another signal that formats and returns the data:
- (RACSignal *)dataSignal
{
return [RACSignal combineLatest:#[self.dataReceivedSignal] reduce:^id(NSNumber * received){
...
return my_data;
}];
}
This view controller just listens to this second signal to get the data.
This works fine.
The problem is, the second time I enter this view controller, I don't want to load the data again, so I save it locally and do this:
if (!self.alreadyHasData) {
self.dataService = [[DataService alloc] init];
self.dataService.delegate = self;
[self.dataService getData];
} else {
self.dataReceivedSignal = [RACSignal return:#YES];
}
In case I already have the data, I'm replacing the dataReceivedSignal with a new one that just sends #YES and completes.
This works too, but that if/else doesn't seem too functional to me. Is this the correct approach?
Thanks.
First of all you can exchange combineLatest to map.
If you want not reload data if it's already loaded, you can write something like this:
- (RACSignal *)dataSignal
{
if (!_dataSignal) {
RACMulticastConnection *dataConnection = [[self.dataReceivedSignal map:^id(NSNumber * received){
/// ...
return my_data;
}] multicast:[RACReplaySubject replaySubjectWithCapacity:1]];
// Only do all of the above after one subscriber has attached.
_dataSignal = [RACSignal defer:^{
[dataConnection connect];
return dataConnection.signal;
}];
}
return _dataSignal;
}
And don't matter how much subscribers signal will have, retrieve data block will be called only one time.
Simpler code = better code. I think you can solve task with simpler solution without RAC.
I'm using the following code to set a property that holds information about twitter rate limits in a core data record:
- (Limits *)limits
{
if (!_limits) {
[[[TwitterManager sharedManager] API] getRateLimitsForResources:nil successBlock:^(NSDictionary *rateLimits) {
_limits = [Limits createWithInfo:rateLimits user:_user];
} errorBlock:^(NSError *error) {
NSLog(#"Error %s: %#", __PRETTY_FUNCTION__, error);
}];
}
return _limits;
}
When it gets to the last line, it doesn't reflect the new value because the block hasn't finished yet. How can I best go about this?
Mixing synchronous and asynchronous code like this is quite hard. The best way to go ahead with this would be to make your limits method asynchronous and have it take an completion block:
- (void)getLimits: (void (^)( Limits *limits ))block
{
if (_limits) {
block( _limits );
} else {
[[[TwitterManager sharedManager] API] getRateLimitsForResources:nil successBlock:^(NSDictionary *rateLimits) {
_limits = [Limits createWithInfo:rateLimits user:_user];
block( _limits );
} errorBlock:^(NSError *error) {
NSLog(#"Error %s: %#", __PRETTY_FUNCTION__, error);
block( nil );
}];
}
}
If you want to call your limits method on the main thread you really should go with this option. Blocking the main thread for network access will not lead to great user experience and might get your app killed by the watchdog if the request takes too long.
If this is not an option you basically have to wait for the request to finish. The easiest way to do this would be to use a synchronous method if your TwitterManager supports that. If not there are different ways to handle this, depending on how the TwitterManager class is implemented.
If everything runs on a different thread you could just use a dispatch group to block your current thread until the response arrived (dispatch_group_wait). This will lead to a deadlock if your twitter class tries to dispatch any work on the same queue/thread you send your limits message on.
The client I'm building is using Reactive Cocoa with Octokit and so far it has been going very well. However now I'm at a point where I want to fetch a collection of repositories and am having trouble wrapping my head around doing this the "RAC way"
// fire this when an authenticated client is set
[[RACAbleWithStart([GHDataStore sharedStore], client)
filter:^BOOL (OCTClient *client) {
return client != nil && client.authenticated;
}]
subscribeNext:^(OCTClient *client) {
[[[client fetchUserRepositories] deliverOn:RACScheduler.mainThreadScheduler]
subscribeNext:^(OCTRepository *fetchedRepo) {
NSLog(#" Received new repo: %#",fetchedRepo.name);
}
error:^(NSError *error) {
NSLog(#"Error fetching repos: %#",error.localizedDescription);
}];
} completed:^{
NSLog(#"Completed fetching repos");
}];
I originally assumed that -subscribeNext: would pass an NSArray, but now understand that it sends the message every "next" object returned, which in this case is an OCTRepository.
Now I could do something like this:
NSMutableArray *repos = [NSMutableArray array];
// most of that code above
subscribeNext:^(OCTRepository *fetchedRepo) {
[repos addObject:fetchedRepo];
}
// the rest of the code above
Sure, this works, but it doesn't seem to follow the functional principles that RAC enables. I'm really trying to stick to conventions here. Any light on capabilities of RAC/Octokit are greatly appreciated!
It largely depends on what you want to do with the repositories afterward. It seems like you want to do something once you have all the repositories, so I'll set up an example that does that.
// Watch for the client to change
RAC(self.repositories) = [[[[[RACAbleWithStart([GHDataStore sharedStore], client)
// Ignore clients that aren't authenticated
filter:^ BOOL (OCTClient *client) {
return client != nil && client.authenticated;
}]
// For each client, execute the block. Returns a signal that sends a signal
// to fetch the user repositories whenever a new client comes in. A signal of
// of signals is often used to do some work in response to some other work.
// Often times, you'd want to use `-flattenMap:`, but we're using `-map:` with
// `-switchToLatest` so the resultant signal will only send repositories for
// the most recent client.
map:^(OCTClient *client) {
// -collect will send a single value--an NSArray with all of the values
// that were send on the original signal.
return [[client fetchUserRepositories] collect];
}]
// Switch to the latest signal that was returned from the map block.
switchToLatest]
// Execute a block when an error occurs, but don't alter the values sent on
// the original signal.
doError:^(NSError *error) {
NSLog(#"Error fetching repos: %#",error.localizedDescription);
}]
deliverOn:RACScheduler.mainThreadScheduler];
Now self.repositories will change (and fire a KVO notification) whenever the repositories are updated from the client.
A couple things to note about this:
It's best to avoid subscribeNext: whenever possible. Using it steps outside of the functional paradigm (as do doNext: and doError:, but they're also helpful tools at times). In general, you want to think about how you can transform the signal into something that does what you want.
If you want to chain one or more pieces of work together, you often want to use flattenMap:. More generally, you want to start thinking about signals of signals--signals that send other signals that represent the other work.
You often want to wait as long as possible to move work back to the main thread.
When thinking through a problem, it's sometimes valuable to start by writing out each individual signal to think about a) what you have, b) what you want, and c) how to get from one to the other.
EDIT: Updated to address #JustinSpahrSummers' comment below.
There is a -collect operator that should do exactly what you're looking for.
// Collect all receiver's `next`s into a NSArray. nil values will be converted
// to NSNull.
//
// This corresponds to the `ToArray` method in Rx.
//
// Returns a signal which sends a single NSArray when the receiver completes
// successfully.
- (RACSignal *)collect;
I have a method on ParentViewModel which returns an RACSequence of ViewModel objects like so:
- (RACSequence *) viewModels
{
return [self.models.rac_sequence map:^id(Model *model) {
return [[ViewModel alloc] initWithModel: model];
}];
}
Each of the ViewModels has a state property on which is an enum and has 3 states: NotStarted, InProgress and Completed. When all the ViewModels in my sequence have the state Completed I know ParentViewModel is valid. I have a validSignal on the ParentViewModel which I want to derive the fact that is valid from the viewModels sequence. At the moment I have this code:
BOOL valid = [[self viewModels] all:^BOOL(ViewModel *vm) {
return vm.state == Completed;
}];
Which gives me an indicator if all ViewModels in the sequence are valid. How can I then turn this into a RACSignal which will update every time the state property on one of the ViewModels changes?
You need first to turn state into a RACSignal, and then everything is easy from that point.
The final code will be something like the following:
RACSignal *valid = [[RACSignal combineLatest:
[[self viewModels] map:^id(ViewModel *viewModel) {
return RACAbleWithStart(viewModel, state);
}]
]
map:^(RACTuple *states) {
return #([states.rac_sequence all:^BOOL(NSNumber *state) {
return state.unsignedIntegerValue == Completed;
}]);
}
];
The first block maps each of your view models into a signal that observes the state property (with the starting value as first value of the signal).
combineLatest: will take a collection of RACSignals and will create a new signal that fires everytime one of the underlaying signals changes, and sends a RACTuple with the value of each signal.
That RACTuple is then converted into a RACSequence, and we can generate a value of #YES or #NO depending if all the values are Completed or not.
I think the result is the signal you were looking for.
(Disclaimer: I’m new to ReactiveCocoa, so there may be an easier way).
I am trying to remove and object from an mutable array - an array which is iterated through every frame (see tick: method).
I am getting
* Collection <__NSArrayM: 0xaa99cb0> was mutated while being enumerated.
exceptions.
So I added #synchronized() to lock it from being touched by other threads, but its still failing.
- (void)addEventSubscriber:(id <EventSubscriber>)eventSubscriber
{
[_eventSubscribers addObject:eventSubscriber];
}
- (void)removeEventSubscriber:(id <EventSubscriber>)eventSubscriber
{
#synchronized(_eventSubscribers) // Not working.
{
[_eventSubscribers removeObject:eventSubscriber];
}
}
- (void)tick:(ccTime)dt
{
for (id <EventSubscriber> subscriber in _eventSubscribers)
{
if ([subscriber respondsToSelector:#selector(tick:)])
{
[subscriber tick:dt];
}
}
}
You need to lock updates to the array completely while iterating. Adding synchronized blocks to both methods addEventSubscriber: and removeEventSubscriber: will not work because the array can change while being iterated over because the iteration is not synchronized. Simply put, only one of those three methods can run at a time.
You can use #synchronized, or an NSLock to manually lock array updates while it is being iterated over.
Alternatively, you could use GCD with a serial dispatch queue to ensure that only one method is executing at a time. Here's how that would work:
You could also store the queue as a property of the class object in which you're doing this processing.
// Create the queue
dispatch_queue_t myQueue = dispatch_queue_create("myQueue", NULL);
- (void)addEventSubscriber:(id <EventSubscriber>)eventSubscriber
{
dispatch_sync(myQueue, ^{
[_eventSubscribers addObject:eventSubscriber];
});
}
- (void)removeEventSubscriber:(id <EventSubscriber>)eventSubscriber
{
dispatch_sync(myQueue, ^{
[_eventSubscribers removeObject:eventSubscriber];
});
}
- (void)tick:(ccTime)dt
{
dispatch_sync(myQueue, ^{
for (id <EventSubscriber> subscriber in _eventSubscribers)
{
if ([subscriber respondsToSelector:#selector(tick:)])
{
[subscriber tick:dt];
}
}
});
}
You are only obtaining a lock while removing items from your array, not while enumerating items. The error suggests that within an enumeration you're trying to remove an item, which is allowed by your locking but not enumeration.
Simply locking the array before enumerating may not work either. The same thread can lock an object recursively, but if your enumeration and remove are on different threads then trying to remove within an enumeration would cause deadlock. If you are in this situation you'll need to rethink your model.
I run into this problem a lot. I have no experience with thread handling / synchronization beyond an undergraduate OS course, so this is what I came up with.
Every time you iterate over the list of objects and want to remove something - instead add that object to a global "objectsToRemove" array. In your update method, remove everything from the objectsToRemove, then clean up the array to avoid over-removing an object on the next update.
Cocos2D has a CCArray which is essentially an NSMutableArray with some added functionality– like being able to remove an item while iterating. I haven't read through the code myself, so I'm not sure how it is implemented and therefore I don't use it.
you need add synchronized in this functin too.
- (void)tick:(ccTime)dt
{
#synchronized(_eventSubscribers){
for (id <EventSubscriber> subscriber in _eventSubscribers)
{
if ([subscriber respondsToSelector:#selector(tick:)])
{
[subscriber tick:dt];
}
}
}
}