Objective C GCD: Do multiple dispatch threads make things faster? - ios

I have some timer code to invoke a AFNetworking 2.0 POST every 2.5 seconds. After experimentation with Xcode simulator and the actual iPhone, I found that on the iPhone, it takes about 5-6seconds before commands are sent out successfully to the server. As a result, the commands called every 2.5 seconds get piled up. On the Xcode simulator, there is no such problem. Commands return the success block in less than a second.
More background:
The aim of POSTing every 2.5 seconds is to keep the TCP connection open. This way, the calls are made even faster compared to opening a new connection every time.
One reason for the lag could be due to the main thread getting too many requests. I have several methods which in turn call more methods.
My question: Should I just use a single custom concurrent queue, or multiple custom concurrent queues? Is this even a good way to solve the networking problem I am facing?
Note: I currently use self.commandSent to track and ensure not too many commands are sent out before the previous POSTs are completed. Will be changing to dispatch groups for this purpose.
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self startTimer];
}
-(void)startTimer
{
dispatch_async(self.myConcurrentQueue, ^{
self.startTime = CFAbsoluteTimeGetCurrent() ;
self.displayTimer = [NSTimer scheduledTimerWithTimeInterval:2.5 target:self selector:#selector(timerFired:) userInfo:nil repeats:YES ] ;
});
}
-(void)timerFired:(NSTimer*)timer
{
//Should I use another dispatch thread here?
CFAbsoluteTime elapsedTime = CFAbsoluteTimeGetCurrent() - self.startTime ;
[self updateDisplay:elapsedTime] ;
}
-(void)stopTimer
{
[self.displayTimer invalidate] ;
self.displayTimer = nil ;
CFAbsoluteTime elapsedTime = CFAbsoluteTimeGetCurrent() - self.startTime ;
[self updateDisplay:elapsedTime] ;
}
-(void)updateDisplay:(CFAbsoluteTime)elapsedTime
{
[self maintainConnection];
}
- (void)maintainConnection {
//Should I use another dispatch thread here?
if (self.commandSent == YES ) {
if(self.temperatureChanged == YES) {
[self updateTemperature]; //updateTemperature and updateSpeed are similar. They call POST requests
[self updateSpeed];
self.temperatureChanged = NO;
} else {
[self updateSpeed];
}
}
}
- (void)updateSpeed {
self.commandSent = NO;
NSURL *baseURL = [NSURL URLWithString:self.BaseURLString];
NSDictionary *parameters = #{#"command": #"16",
#"dat": self.dataToSend,};
//Create instance of AFHTTPSessionManager and set response to default JSON
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
[manager POST:#"submit" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
self.commandSent = YES;
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(#"%#", [error localizedDescription]);
}];
}

AFNetworking is an async API built on top of Apples NSURLConnection and NSURLSession classes. It handles the background processes for you, and you should not try to add threading on top of the async logic already built into the library.
Concurrent programming can make certain tasks faster because on a multi-core system, you get all available cores doing part of a job at the same time, like having multiple workers building a house instead of just one.
Like building a house, though, there are dependencies. You can't pour the foundation until the sewer pipe and utility feeds are in place. You can't build the frame until the foundation is poured. You can't put the roof on until the walls are up. You can't hang drywall until the roof is on and the house is water-tight.
Similarly, when you break a computer task up to run it concurrently, there are often inter-dependencies between the tasks. In graphics processing, you have to do your vertex calculations first before you can render textures onto the resulting surfaces.
Your questions tell me that you don't really understand the concepts behind concurrent programming. There be dragons! Concurrent programming is very tricky business, the number of ways you can screw it up, large and small, are almost infinite, and the resulting problems are very difficult to diagnose and fix.
If you don't know about race conditions, locks, spin locks, semaphores, and such, stay away from concurrent programing. You will wind up with an unstable mess.

Related

How to improve this objective-c code (blocks, RestKit, async, threads)

I'm maintaining an old game code (>5 yrs old) and switched developers hands a few times. Game doesn't has a dedicated player base (an early casino gambling game).
RestKit is used for API calls.
Please find comments: // SECTION_1 // SECTION_2 in the code below.
// SECTION_1 : can make it async, use blocking logic. What are the some immediate risks related to introducing threading bugs?
// SECTION_2 : Need to fix a bug bug in previous logic here. Bug: self.fetchAllPlayersCallback gets invoked before waiting for self.fetchAllPlayersFriendCheckCallback. For correct UI update, I would need to combine self.fetchAllPlayersFriendCheckCallback and self.fetchAllPlayersCallback.
Code:
/* getAllPlayersInGame:(NSString *)gameId
* Fetch players for a game in progress, update UI, invoke fetchAllPlayersCallback
* Also detect if players are friends. Prepare friends set and invoke fetchAllPlayersFriendCheckCallback.
*/
- (void)getAllPlayersInGame:(NSString *)gameId
{
self.fetchAllPlayersInProgress = YES;
self.fetchAllPlayersError = nil;
[SocialManager getPlayersAndProfilesForGameId:gameId userId:[UserManager getActiveUser] completion:^(NSError *error, SocialUsers *users, SocialProfiles *profiles)
{
if (error) {
self.fetchAllPlayersError = error;
// TODO: show ui error alert
return;
}
__block NSUInteger totalusers = [self.lobby.players count];
__block BOOL isAllPlayersFriends = YES;
__block NSMutableSet *friendsInGame = [[NSMutableSet alloc] init]
// SECTION_1
// separate lightweight call to server per player.
// server implementation limitation doesn't allow sending bulk requests.
for (SocialUser *player in self.lobby.players) {
NSString *playerId = player.playerID;
[SocialManager isUser:userId friendsWithPlayer:playerId completionBlock:^(PlayHistory *playHistory, NSError *error) {
totalusers--;
if (!error) {
isAllPlayersFriends &= playHistory.isFriend;
if (playHistory.isFriend)
{
// TODO: Add to friendsInGame
// TODO: save other details (game history, etc for ui population)
}
} else {
self.fetchAllPlayersFriendCheckCallback(isAllPlayersFriends, friendsInGame, error);
return;
}
if (0 == totalusers) {
fetchAllPlayersFriendCheckCallback(isAllPlayersFriends, friendsInGame, error);
}
}];
};
// SECTION_2
// TODO: update data model
// TODO: UI update view
self.fetchAllPlayersInProgress = NO;
if (self.fetchAllPlayersCallback)
{
self.fetchAllPlayersCallback();
self.fetchAllPlayersCallback = nil;
}
}];
}
There are a few approaches:
If you have a bunch of asynchronous requests that can happen concurrently with respect to each other and you want to trigger some other task when they're done, you might use Grand Central Dispatch (GCD) dispatch groups.
For example, rather than counting down totalUsers, the standard GCD approach is to use a dispatch group. Dispatch groups can trigger some block that will be called when a bunch of asynchronous calls are done. So you:
Create a group before you start your loop;
Enter your group before you start asynchronous call;
Leave your group in the asynchronous call's completion handler;
Specify a dispatch_group_notify block that will be called when each "enter" is matched with a "leave".
Thus, something like:
dispatch_group_t group = dispatch_group_create();
for (SocialUser *player in self.lobby.players) {
dispatch_group_enter(group);
[SocialManager ...: ^{
...
dispatch_group_leave(group);
}];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
fetchAllPlayersFriendCheckCallback(isAllPlayersFriends, friendsInGame, error);
self.fetchAllPlayersInProgress = NO;
if (self.fetchAllPlayersCallback) {
self.fetchAllPlayersCallback();
self.fetchAllPlayersCallback = nil;
}
});
Now, this presumes that this call is asynchronous but that they can run concurrently with respect to each other.
Now, if these asynchronous calls need to be called consecutively (rather than concurrently), then you might wrap them in asynchronous NSOperation or something like that, which assures that even if they're running asynchronously with respect to the main queue, they'll run consecutively with respect to each other. And if you use that approach, rather than using a dispatch group for the completion operations, you would use NSOperation dependencies. For example, here's a trivial example:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
NSOperation *completion = [NSBlockOperation blockOperationWithBlock:^{
// stuff to be done when everything else is done
}];
for (Foo *foo in self.foobars) {
NSOperation *operation = [SocialManager operationForSomeTask:...];
[completionOperation addDependency:operation];
[queue addOperation:operation];
}
[[NSOperationQueue mainQueue] addOperation:completionOperation];
But all of this assumes that you're refactored your social manager to wrap its asynchronous requests in custom asynchronous NSOperation subclass. It's not rocket science, but if you haven't done that before, you might want to gain familiarity with creating them before you tackle refactoring your existing code to do so.
Another permutation of the previous point is that rather than refactoring your code to use custom asynchronous NSOperation subclasses, you could consider a framework like PromiseKit. It still requires you to refactor your code, but it has patterns that let you wrap your asynchronous task in "promises" (aka "futures"). I only mention it for the take of completeness. But you might not want to throw a whole new framework in this mix.
Bottom line, there's simply not enough here to diagnose this. But dispatch groups or custom asynchronous NSOperation subclasses with completion operations.
But the comment in that code that says "use blocking logic" is generally not a good idea. You should never block and with well designed code, it's completely unnecessary.

WCSession sendmessage is twice as slow after updating from 2.1 to 2.2

The iOS application that I work on has an apple watch app that goes along with it. We recently started getting complaints that the GPS distance updates have slowed down and the watch is a few seconds behind the phone. I have been looking into this and wrote some test code, the reply block from [[WCSession defaultSession] sendMessage:message
replyHandler:replyHandler
errorHandler:errorHandler
is definitely twice as slow in watchOS 2.2 vs 2.1. I have attached the test code below.
#pragma mark - Location Update.
/**
* #description Provides an NSBlockOperation to be executed in an operation queue. This is an attempt to force serial
* processing
*/
- (NSBlockOperation*)distanceUpdateBlock {
NSBlockOperation *blockOp = [[NSBlockOperation alloc] init];
__weak NSBlockOperation * weakOp = blockOp;
__weak typeof(self) weakSelf = self;
[blockOp addExecutionBlock:^{
typeof(weakSelf) blockSafeSelf = weakSelf;
typeof(weakOp) blockSafeOp = weakOp;
if (!blockSafeOp.isCancelled) { // Make sure we haven't already been cancelled.
__block NSDate *startDate = [NSDate date];
__block BOOL completed = NO;
void (^replyBlock)(NSDictionary*) = ^(NSDictionary *message){
if (!blockSafeOp.isCancelled) {
[blockSafeSelf processUserLocationOnWatch:message];
double replyTime = [[NSDate date] timeIntervalSinceDate:startDate];
NSLog(#"Reply Time: %.03f", replyTime);
completed = YES;
}
};
void (^failBlock)(NSError*) = ^(NSError *error) {
if (!blockSafeOp.isCancelled) {
double replyTime = [[NSDate date] timeIntervalSinceDate:startDate];
NSLog(#"Reply Time Fail: %.03f", replyTime);
completed = YES;
}
};
[self fetchUserLocationFromIphoneWithReplyHandler:replyBlock errorHandler:failBlock];
do {
usleep(10000); // 1/100th second wait to throttle evaluations (Don't worry - in final code I will subclass NSOperation and control when it actually finishes - this is for easy testing.)
} while (!completed && !blockSafeOp.isCancelled && [blockSafeSelf isWatchReachable]); //(isWatchReachable just makes sure we have a session with the phone and it is reachable).
}
}];
blockOp.completionBlock = ^{
typeof(weakSelf) blockSafeSelf = weakSelf;
typeof(weakOp) blockSafeOp = weakOp;
if (!blockSafeOp.isCancelled) {
[blockSafeSelf addOperationForLocationUpdate]; // since we are finished - add another operation.
}
};
return blockOp;
}
- (void)addOperationForLocationUpdate {
[distanceUpdateOperationQueue addOperation:[self distanceUpdateBlock]];
}
- (void)startUpdatingLocation {
[self addOperationForLocationUpdate];
}
- (void)stopUpdatingLocation {
[distanceUpdateOperationQueue cancelAllOperations];
}
- (void)fetchUserLocationFromIphoneWithReplyHandler:(nullable void (^)(NSDictionary<NSString *, id> *replyMessage))replyHandler errorHandler:(nullable void (^)(NSError *error))errorHandler {
if (self.isSessionActive) {
NSDictionary *message = #{kWatchSessionMessageTag:kWatchSessionMessageUserLocation};
if (self.isWatchReachable) {
[[WCSession defaultSession] sendMessage:message
replyHandler:replyHandler
errorHandler:errorHandler
];
} else {
errorHandler(nil);
}
} else {
[self activateSession];
errorHandler(nil);
}
}
The handler on the iPhone side simply get's the User location and calls the replyHandler with the encoded information.
The logs for time on 2.2 look like (consistently about a second)
Reply Time: 0.919
Reply Time: 0.952
Reply Time: 0.991
Reply Time: 0.981
Reply Time: 0.963
Same code on 2.1 looks like
Reply Time: 0.424
Reply Time: 0.421
Reply Time: 0.433
Reply Time: 0.419
Also, I've noticed that after 5 mins (300 seconds) the error handlers start getting called on the messages that have already had the reply handler called. I saw another thread where someone mentioned this as well, is anyone else having this happen and know why?
So, Q1 - has anyone run into this performance slow down and figured out how to keep the replyHandler running faster, or found a faster way to get updates?
Q2 - Solution for the errorHandler getting called after 5 mins.
Just a few things to eliminate - I have done my due diligence on testing the iOS code between receiving the message and calling the replyHandler. There is no change in the time to process between iOS 9.2/9.3. I have narrowed it down to this call. In fact, the way we did this in previous versions is now backing up the sendMessage's operationQueue. So now I am forcing a one at a time call with this test code. We don't get backed up anymore, but the individual calls are slow.
So, I was running tests today on the same piece of code, using the same devices, and although the code has not changed, it is now running twice as fast as it initially did (in 2.1). Logs are coming in the range of .12 - .2 seconds. Only thing that has happened since then is software updates. Also, the fail block is no longer called after 5 mins. So both parts of this question are magically working. I am currently using iOS 9.3.4(13G35) and watch is at 2.2.2. Seems to have been an OS issue somewhere in the chain of processing the queue between the watch and the phone. All is good now.

Why won't this loop exit

My assumption is that the operations are running asynchronously on a separate thread, but the loop never exits, so something is not as I assumed.
/**
Checks if we can communicate with the APIs
#result YES if the network is available and all of the registered APIs are responsive
*/
- (BOOL)apisAvailable
{
// Check network reachability
if (!_connectionAvailable) {
return NO;
}
// Check API server response
NSMutableSet *activeOperations = [[NSMutableSet alloc] init];
__block NSInteger successfulRequests = 0;
__block NSInteger failedRequests = 0;
for (AFHTTPClient *httpClient in _httpClients) {
// Send heart beat request
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:#"" parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// Server returned good response
successfulRequests += 1;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Server returned bad response
failedRequests += 1;
}];
[operation start];
[activeOperations addObject:operation];
}
// Wait for heart beat requests to finish
while (_httpClients.count > (successfulRequests + failedRequests)) {
// Wait for each operation to finish, one at a time
//usleep(150);
[NSThread sleepForTimeInterval:0.150];
}
// Check final results
if (failedRequests > 0) {
return NO;
}
return YES;
}
A few suggestions:
Never check reachability to determine if a request will succeed. You should try the request; only if it fails should you consult reachability to try and get a best guess as to why. Reachability makes no guarantee about whether a request will fail or succeed.
Is this method called on the main thread? Even if you fixed the problem with the requests never completing, it will block the UI the entire time your network requests are running. Since these requests can take potentially a long time, this is a bad experience for the user as well as something the OS will kill your app for if it happens at the wrong time (e.g. at launch).
Looping while calling sleep or equivalent is wasteful of CPU resources and memory, as well as prevents the thread's runloop from servicing any timers, event handler or callbacks. (Which is probably why the networking completion blocks never get to run.) If you can avoid blocking a thread, you should. In addition, Cocoa will very often be unhappy if you do this on an NSThread you didn't create yourself.
I see two options:
Use dispatch_groups to wait for all of your requests to finish. Instead of blocking your calling thread, you should instead take a completion block to call when you're done. So, instead of returning a BOOL, take a completion block which takes a BOOL. Something like - (void)determineIfAPIIsAvailable:(void(^)(BOOL))completionBlock;
Get rid of this method altogether. What are you using this method for? It's almost certainly a better idea to just try to use your API and report appropriate errors to the user when things fail rather than to try to guess if a request to the API will succeed beforehand.
I believe the issue is that I was not using locking to increment the counters so the while loop would never evaluate to true.
I was able to get it working by only looking for a fail count greater than 0 that way as long as it was incremented by any of the request callback blocks then I know what to do.
I just so happen to have switched to [NSOperationQueue waitUntilAllOperationsAreFinished].
Final code:
/**
Checks if we can communicate with the APIs
#result YES if the network is available and all of the registered APIs are responsive
*/
- (BOOL)apisAvailable
{
// Check network reachability
if (!_connectionAvailable) {
return NO;
}
// Check API server response
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
__block NSInteger failedRequests = 0;
for (AFHTTPClient *httpClient in _httpClients) {
// Send heart beat request
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET" path:#"" parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
// Server returned good response
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Server returned bad response
failedRequests += 1;
}];
[operationQueue addOperation:operation];
}
// Wait for heart beat requests to finish
[operationQueue waitUntilAllOperationsAreFinished];
// Check final results
if (failedRequests > 0) {
return NO;
}
return YES;
}

Send a bunch of requests one-by-one [closed]

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
Questions asking for code must demonstrate a minimal understanding of the problem being solved. Include attempted solutions, why they didn't work, and the expected results. See also: Stack Overflow question checklist
Closed 9 years ago.
Improve this question
I need to send 100 network requests to my server one-by-one and get notified when the 100th is done.
I'm using AFNetworking and was thinking about a solution of this problem. Can anyone recommend me something?
A couple of thoughts:
If really just going to run each request serially (i.e. one after another), you could do:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"All operations done");
}];
for (NSInteger i = 0; i < operationCount; i++) {
AFHTTPRequestOperation *operation = ... // create your operation here
[completionOperation addDependency:operation];
[queue addOperation:operation];
}
[queue addOperation:completionOperation];
Note, using operation queue like this offers the advantage that you can easily cancel all the operations in that queue should you ever need to.
If the order that these are performed is critical, you might want to establish explicit dependencies between the operations, e.g.:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"All operations done");
}];
NSOperation *priorOperation = nil;
for (NSInteger i = 0; i < operationCount; i++) {
AFHTTPRequestOperation *operation = ... // create your operation here
[completionOperation addDependency:operation];
if (priorOperation) [operation addDependency:priorOperation];
[queue addOperation:operation];
priorOperation = operation;
}
[queue addOperation:completionOperation];
The question for me is whether you absolutely only want to run one at a time. You pay a significant performance penalty for that. Generally you'd use that first code sample (where the only explicit dependencies are to the completion operation) and set maxConcurrentOperationCount to something like 4, enjoying concurrency and its consequent significant performance gain (while at the same time, constraining the degree of concurrency to some reasonable number that won't use up all of your worker threads, risk having requests time out, etc.).
You haven't said what these 100 operations are, but if it's a bunch of downloads, you might want to consider a "lazy loading" pattern, loading the data asynchronously as you need it, rather than all at once.
If downloading images, for example, you might achieve this using the AFNetworking UIImageView category.
This is a specific form of a common question, which is "how do I call a sequence of block operations and get notified when the last one finishes?"
One idea is to make a "to-do list" using the parameters for each request. Say each request takes a number 0..99. Now pseudo-code would looks like this:
#property(nonatomic, copy) void (^done)(BOOL); // we'll need to save a completion block
#property(nonatomic, strong) NSMutableArray *todo; // might as well save this too
- (void)makeRequestsThenInvoke:(void (^)(BOOL))done {
self.todo = [NSMutableArray arrayWithArray:#[#99, #98, #97 ... #0]];
// make this in a loop using real params to your network request (whatever distinguishes each request)
self.done = done;
[self makeRequests];
}
- (void)makeRequests {
if (!self.todo.count) { // nothing todo? then we're done
self.done(YES);
self.done = nil; // avoid caller-side retain cycle
return;
}
// otherwise, get the next item todo
NSNumber *param = [self.todo lastObject];
// build a url with param, e.g. http://myservice.com/request?param=%# <- param goes there
[afManager post:url success:success:^(AFHTTPRequestOperation *operation, id responseObject) {
// handle the result
// now update the todo list
[self.todo removeLastObject];
// call ourself to do more, but use performSelector so we don't wind up the stack
[self performSelector:#selector(makeRequests) withObject:nil afterDelay:0.0];
}];
}

Managing a bunch of NSOperation with dependencies

I'm working on an application that create contents and send it to an existing backend. Content is a title, a picture and location. Nothing fancy.
The backend is a bit complicated so here is what I have to do :
Let the user take a picture, enter a title and authorize the map to use its location
Generate a unique identifier for the post
Create the post on the backend
Upload the picture
Refresh the UI
I've used a couple of NSOperation subclasses to make this work but I'm not proud of my code, here is a sample.
NSOperation *process = [NSBlockOperation blockOperationWithBlock:^{
// Process image before upload
}];
NSOperation *filename = [[NSInvocationOperation alloc] initWithTarget: self selector: #selector(generateFilename) object: nil];
NSOperation *generateEntry = [[NSInvocationOperation alloc] initWithTarget: self selector: #selector(createEntry) object: nil];
NSOperation *uploadImage = [[NSInvocationOperation alloc] initWithTarget: self selector: #selector(uploadImageToCreatedEntry) object: nil];
NSOperation *refresh = [NSBlockOperation blockOperationWithBlock:^{
// Update UI
[SVProgressHUD showSuccessWithStatus: NSLocalizedString(#"Success!", #"Success HUD message")];
}];
[refresh addDependency: uploadImage];
[uploadImage addDependency: generateEntry];
[generateEntry addDependency: filename];
[generateEntry addDependency: process];
[[NSOperationQueue mainQueue] addOperation: refresh];
[_queue addOperations: #[uploadImage, generateEntry, filename, process] waitUntilFinished: NO];
Here are the things I don't like :
in my createEntry: for example, I'm storing the generated filename in a property, which mees with the global scope of my class
in the uploadImageToCreatedEntry: method, I'm using dispatch_async + dispatch_get_main_queue() to update the message in my HUD
etc.
How would you manage such workflow ? I'd like to avoid embedding multiple completion blocks and I feel like NSOperation really is the way to go but I also feel there is a better implementation somewhere.
Thanks!
You can use ReactiveCocoa to
accomplish this pretty easily. One of its big goals is to make this kind of
composition trivial.
If you haven't heard of ReactiveCocoa before, or are unfamiliar with it, check
out the Introduction
for a quick explanation.
I'll avoid duplicating an entire framework overview here, but suffice it to say that
RAC actually offers a superset of promises/futures. It allows you to compose and
transform events of completely different origins (UI, network, database, KVO,
notifications, etc.), which is incredibly powerful.
To get started RACifying this code, the first and easiest thing we can do is put
these separate operations into methods, and ensure that each one returns
a RACSignal. This isn't strictly necessary (they could all be defined within
one scope), but it makes the code more modular and readable.
For example, let's create a couple signals corresponding to process and
generateFilename:
- (RACSignal *)processImage:(UIImage *)image {
return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
// Process image before upload
UIImage *processedImage = …;
[subscriber sendNext:processedImage];
[subscriber sendCompleted];
}];
}
- (RACSignal *)generateFilename {
return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
NSString *filename = [self generateFilename];
[subscriber sendNext:filename];
[subscriber sendCompleted];
}];
}
The other operations (createEntry and uploadImageToCreatedEntry) would be very similar.
Once we have these in place, it's very easy to compose them and express their
dependencies (though the comments make it look a bit dense):
[[[[[[self
generateFilename]
flattenMap:^(NSString *filename) {
// Returns a signal representing the entry creation.
// We assume that this will eventually send an `Entry` object.
return [self createEntryWithFilename:filename];
}]
// Combine the value with that returned by `-processImage:`.
zipWith:[self processImage:startingImage]]
flattenMap:^(RACTuple *entryAndImage) {
// Here, we unpack the zipped values then return a single object,
// which is just a signal representing the upload.
return [self uploadImage:entryAndImage[1] toCreatedEntry:entryAndImage[0]];
}]
// Make sure that the next code runs on the main thread.
deliverOn:RACScheduler.mainThreadScheduler]
subscribeError:^(NSError *error) {
// Any errors will trickle down into this block, where we can
// display them.
[self presentError:error];
} completed:^{
// Update UI
[SVProgressHUD showSuccessWithStatus: NSLocalizedString(#"Success!", #"Success HUD message")];
}];
Note that I renamed some of your methods so that they can accept inputs from
their dependencies, giving us a more natural way to feed values from one
operation to the next.
There are huge advantages here:
You can read it top-down, so it's very easy to understand the order that
things happen in, and where the dependencies lie.
It's extremely easy to move work between different threads, as evidenced by
the use of -deliverOn:.
Any errors sent by any of those methods will automatically cancel all the
rest of the work, and eventually reach the subscribeError: block for easy
handling.
You can also compose this with other streams of events (i.e., not just
operations). For example, you could set this up to trigger only when a UI
signal (like a button click) fires.
ReactiveCocoa is a huge framework, and it's unfortunately hard to distill the
advantages down into a small code sample. I'd highly recommend checking out the
examples for when to use
ReactiveCocoa
to learn more about how it can help.
A couple of thoughts:
I would be inclined to avail myself of completion blocks because you probably only want to initiate the next operation if the previous one succeeded. You want to make sure that you properly handle errors and can easily break out of your chain of operations if one fails.
If I wanted to pass data from operation to another and didn't want to use some property of the caller's class, I would probably define my own completion block as a property of my custom operation that had a parameter which included the field that I wanted to pass from one operation to another. This assumes, though, that you're doing NSOperation subclassing.
For example, I might have a FilenameOperation.h that defines an interface for my operation subclass:
#import <Foundation/Foundation.h>
typedef void (^FilenameOperationSuccessFailureBlock)(NSString *filename, NSError *error);
#interface FilenameOperation : NSOperation
#property (nonatomic, copy) FilenameOperationSuccessFailureBlock successFailureBlock;
#end
and if it wasn't a concurrent operation, the implementation might look like:
#import "FilenameOperation.h"
#implementation FilenameOperation
- (void)main
{
if (self.isCancelled)
return;
NSString *filename = ...;
BOOL failure = ...
if (failure)
{
NSError *error = [NSError errorWithDomain:... code:... userInfo:...];
if (self.successFailureBlock)
self.successFailureBlock(nil, error);
}
else
{
if (self.successFailureBlock)
self.successFailureBlock(filename, nil);
}
}
#end
Clearly, if you have a concurrent operation, you'll implement all of the standard isConcurrent, isFinished and isExecuting logic, but the idea is the same. As an aside, sometimes people will dispatch those success or failures back to the main queue, so you can do that if you want, too.
Regardless, this illustrates the idea of a custom property with my own completion block that passes the appropriate data. You can repeat this process for each of the relevant types of operations, you can then chain them all together, with something like:
FilenameOperation *filenameOperation = [[FilenameOperation alloc] init];
GenerateOperation *generateOperation = [[GenerateOperation alloc] init];
UploadOperation *uploadOperation = [[UploadOperation alloc] init];
filenameOperation.successFailureBlock = ^(NSString *filename, NSError *error) {
if (error)
{
// handle error
NSLog(#"%s: error: %#", __FUNCTION__, error);
}
else
{
generateOperation.filename = filename;
[queue addOperation:generateOperation];
}
};
generateOperation.successFailureBlock = ^(NSString *filename, NSData *data, NSError *error) {
if (error)
{
// handle error
NSLog(#"%s: error: %#", __FUNCTION__, error);
}
else
{
uploadOperation.filename = filename;
uploadOperation.data = data;
[queue addOperation:uploadOperation];
}
};
uploadOperation.successFailureBlock = ^(NSString *result, NSError *error) {
if (error)
{
// handle error
NSLog(#"%s: error: %#", __FUNCTION__, error);
}
else
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// update UI here
NSLog(#"%#", result);
}];
}
};
[queue addOperation:filenameOperation];
Another approach in more complicated scenarios is to have your NSOperation subclass employ a technique analogous to how the standard addDependency method works, in which NSOperation sets the isReady state based upon KVO on isFinished on the other operation. This not only allows you to not only establish more complicated dependencies between operations, but also to pass database between them. This is probably beyond the scope of this question (and I'm already suffering from tl:dr), but let me know if you need more here.
I wouldn't be too concerned that uploadImageToCreatedEntry is dispatching back to the main thread. In complicated designs, you might have all sorts of different queues dedicated for particular types of operations, and the fact that UI updates are added to the main queue is perfectly consistent with this mode. But instead of dispatch_async, I might be inclined to use the NSOperationQueue equivalent:
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// do my UI update here
}];
I wonder if you need all of these operations. For example, I have a hard time imagining that filename is sufficiently complicated to justify its own operation (but if you're getting the filename from some remote source, then a separate operation makes perfect sense). I'll assume that you're doing something sufficiently complicated that justifies it, but the names of those operations make me wonder, though.
If you want, you might want to take a look at couchdeveloper's RXPromise class which uses promises to (a) control the logical relationship between separate operations; and (b) simplify the passing of data from one to the next. Mike Ash has a old MAFuture class which does the same thing.
I'm not sure either of those are mature enough that I'd contemplate using them in my own code, but it's an interesting idea.
I'm probably totally, biased - but for a particular reason - I like #Rob's approach #6 ;)
Assuming you created appropriate wrappers for your asynchronous methods and operations which return a Promise instead of signaling the completion with a completion block, the solution looks like this:
RXPromise* finalResult = [RXPromise all:#[[self filename], [self process]]]
.then(^id(id filenameAndProcessResult){
return [self generateEntry];
}, nil)
.then(^id(id generateEntryResult){
return [self uploadImage];
}, nil)
.thenOn(dispatch_get_main_queue() , ^id(id uploadImageResult){
[self refreshWithResult:uploadImageResult];
return nil;
}, nil)
.then(nil, ^id(NSError*error){
// Something went wrong in any of the operations. Log the error:
NSLog(#"Error: %#", error);
});
And, if you want to cancel the whole asynchronous sequence at any tine, anywhere and no matter how far it has been proceeded:
[finalResult.root cancel];
(A small note: property root is not yet available in the current version of RXPromise, but its basically very simple to implement).
If you still want to use NSOperation, you can rely on ProcedureKit and use the injection properties of the Procedure class.
For each operation, specify which type it produces and inject it to the next dependent operation. You can also at the end wrap the whole process inside a GroupProcedure class.

Resources