views:

105

answers:

3

Let's say that if I read from www.example.com/number, I get a random number. In my iPhone app, I want to be able to continuously read from that address and display the new number on the screen after each request is finished. Let's also assume that I want this process to start as soon as the view loads. Lastly, as a side-note, I'm using ASIHTTPRequest to simplify the web requests.

Approach 1: In my viewDidLoad method I could synchronously read from the URL in a loop (execution will not continue until I get a response from the HTTP request). Pros: the requests are serial and I have full control to respond to each one. Cons: the UI never gets updated because I never exit the function and give control back to the run time loop. Clearly, this is not a good solution.

Approach 2: In my viewDidLoad method I create a timer which calls a fetchURL function once per second. Pros: each request is in a separate thread, and the UI updates after each request is finished. Cons: the requests are in separate threads, and cannot be controlled well. For example, if there is a connection timeout on the first request, I want to be able to display an error popup, and not have any further requests happen until settings are changed. However, with this approach, if it takes 3 seconds to timeout, two additional requests will have already been started in that time. If I just slow down the timer, then data comes in too slowly when the connection is working well.

It seems like there should be some approach which would merge the benefits of the first two approaches I mentioned. I would like a way that I could decide whether on not to send the next request based on the result of the previous request.

Approach 3: I considered using a timer which fires more quickly (say every .25 seconds), but have the timer's function check a flag to see what to do next. So, if the previous request has finished, it sends a new request (unless there was an error). Otherwise, if the previous request has not finished, the timer's function returns without sending a new request. By firing this timer more quickly, you would get better response time, but the flag would let me get the synchronization I wanted.

It seems like Approach 3 would do what I want, but it also seems a little forced. Does anyone have a suggestion for a better approach to this, or is something like Approach 3 the best way to do it?

+1  A: 

I believe an NSOperation is what you need. Use the number 1 solution above, but place the code in your NSOperation's main method. Something like this:

The .h file

@interface MyRandomNumberFetcher : NSOperation {

}

@end

The .m file

@implementation MyRandomNumberFetcher

- (void) main {
      // This is where you start the web service calls.
}

@end

I'd also recommend adding a reference to the UI controller so your operation queue class can call it back when it's appropriate.

NWCoder
A: 

Here's another suggestion. Create an NSOperationQueue that will run your requests on a different thread. If you find you need to refresh the UI call performSelectorOnMainThread. When the request completes create another request and add it to the queue. Set the queue to run only one action at a time.

This way you'll never have two requests running at the same time.

Ron Srebro
Thanks, Ron! NSOperations are new to me... So, I create the queue in my ViewController and add an initial NSOperation. The operation runs and downloads a number. In the operation code, would I be correct to send the performSelectorOnMainThread message to the ViewController object? That function called would be responsible for updating the IBOutlet. Then, should I have the NSOperation notify the ViewController that it's finished successfully (or unsuccessfully) so that it can either add or not add a new NSOperation to the queue?
@NWCoder. Exactly as you described. But I do suggest checking @Mo suggestion, was just today looking into GCD myself.
Ron Srebro
+1  A: 

You could do this using GCD with less code and using fewer resources. This is how you could do it:

In viewDidLoad call a block asynchronously (using dispatch_async) that does the following:

  1. Load the data with a synchronous call and handle timeouts if it failed.
  2. If successful, inform the main thread to update the UI.
  3. Queue a new block to run after a delay that does the same thing (using dispatch_after).

To call back to the main thread from another thread I can think of these methods:

  1. If you want to update a custom view, you can set setNeedsDisplay from your block
  2. Otherwise, you could queue a block on what's called "main queue", which is a queue running on the main thread. You get this queue by calling dispatch_get_main_queue. and then treat it like any other queue (for example you can add your block by calling dispatch_async).
  3. If you don't want to use blocks you can use the NSObject's performSelectorOnMainThread:withObject:waitUntilDone: method.

See GCD Reference for more details.

That said, you should never keep performing small requests so frequently (unless for specific tasks like fetching game data or something). It will severely reduce battery life by keeping antenna from sleeping.

Mo
Thanks, Mo! GCD is a new topic for me as well, so I'll do some research on it. The process seems to make sense, but could you clarify the 2nd step? How does the block go about sending the notification to the main thread?
See the updated answer. Hope it helps.
Mo