I'm currently kicking off a background thread to do some REST queries in my app delegate's didFinishLaunchingWithOptions. This thread creates some objects and populates the model as the rest of the app continues to load (because I don't block, and didFinishLaunchingWithOptions returns YES). I also put up a loading UIViewController 'on top' of the main view that I tear down after the background initialization is complete.
My problem is that I need to notify the first view (call it the Home view) that the model is ready, and that it should populate itself. The trick is that the background download could have finished before Home.viewDidAppear is called, or any of the other Home.initX methods.
I'm having difficulty synchronizing all of this and I've thought about it long enough that it feels like I'm barking up the wrong tree.
Are there any patterns here for this sort of thing? I'm sure other apps start by performing lengthy operations with loading screens :)
Thanks!
I usually have a Factory class that’s responsible for wiring all the objects together. The Factory is created by the application delegate in the applicationDidFinishLaunching:
- (void) applicationDidFinishLaunching: (UIApplication*) app {
// Creates all the objects that are needed to wire
// the application controllers. Can take long. We are
// currently displaying the splash screen, so that we
// can afford to block for a moment.
factory = [[Factory alloc] init];
// Now that we have the building blocks, we can wire
// the home screen and start the application.
home = [[factory wireHomeScreen] retain];
[window addSubview:home.view];
[window makeKeyAndVisible];
}
Now if the Factory creation takes long, I simply wait under the splash screen or put up another view that displays spinner until everything is ready. I guess you could use this very scheme if you can perform the initialization synchronously:
#implementation Factory
- (id) init {
[super init];
// Takes long, performs the network I/O.
someDataSource = [[DataSource alloc] init…];
return self;
}
- (id) wireHomeScreen {
// Data source already loaded or failed to load.
HomeScreen *home = [[HomeScreen alloc] init…];
[home setDataSource:someDataSource];
return [home autorelease];
}
#end
With a bit of luck there’s just a single long operation in your startup routine, so that you won’t lose anything by serializing the init.
If you want to perform the data source init in background, you can display some introductory screen that will cue the home screen once the data has been loaded:
- (void) applicationDidFinishLaunching: (UIApplication*) app
{
// Create the basic building blocks to wire controllers.
// Will not load the data from network, not yet.
factory = [[Factory alloc] init];
// Display something while the data are being loaded.
IntroScreen *intro = [[IntroScreen alloc] init];
// Main screen, will get displayed once the data are loaded.
home = [[factory wireHomeScreen] retain];
// The intro screen has to know what do display next.
[intro setNextScreen:home];
// Start loading data and then notify the intro screen
// that we are done loading and the show can begin.
[factory.someDataSource startLoadingAndNotify:intro];
[window addSubview:intro.view];
[window makeKeyAndVisible];
}
Now when the data source has finished loading the data, it will tell the intro screen to cue the real content (home screen, in this case). This is just a rough sketch (for example the memory management might be different in the real case), but in principle it should work fine.
I ran into a similar issue a while back writing a REST application. It wasn't at the home screen but the general idea was the same. Issues with synchronizing call backs with NSURLRequests and so on. I found an example code from Apple that doesn't really solve this problem but illustrates how Apple solved it. Here's the link...
http://developer.apple.com/iphone/library/samplecode/XMLPerformance/Introduction/Intro.html
They do block the thread, so maybe it's not the solution you are looking for. In short, they use...
-(void) get {
finished = NO;
... do threading stuff ...
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (!finished);
}
Just make sure your thread calls set finished to YES at some point.
It's not a pattern by any means, but I found it very helpful if you are using multiple threads in your application.
Related
I feel as though there is a really simple solution to my problem, but thus far I have had little success... I want to load my initial .xib file (exiting the default.png splash screen early), so that I may display an activity indicator while loading my html data and setting the text label fields created by my xib file.
Unfortunately, when I execute the following code below, I display my default.png image until all of the data is loaded from each website... What may I change in order to first display my mainView, then start the activity indicator, load my html data, and set the text labels in my mainView?
#implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];
[activityIndicator startAnimating];
[self runTimer];
}
- (void)viewDidAppearBOOL)animated {
[super viewDidAppear:animated];
[self loadHTMLData1];
[self loadHTMLData2];
[self loadHTMLData3];
[self loadHTMLData4];
[activityIndicator stopAnimating];
}
...
It's all to do with how iOS updates the ui. When you call
[activityIndicator startAnimating];
it doesn't mean start animating immediately, it means you're telling the ui the next time you are updating the display, start animating.
All of this updating happens on the main thread (if you haven't made a thread, you're already on the main thread) so if you do something else that takes a long time, it will do this before updating the display.
There are a few ways to fix this and they all involve making another thread that runs in the background.
Take a look at NSOperation (and NSOperationQueue) - this will let you queue up individual tasks that iOS will run in the background for you. then when they are complete you can update your display again and turn off your activity indicator.
There's NSOperationQueue tutorials all over google :)
Hope that helps.
I had the UIActivityIndicatorView working fine in simulator and other 3.0 devices in my app. But I found out that it was not spinning (or showing) in the new iphone 4. Basically I need to show the activity indicator when a button is clicked and hide it when the button click event is complete. I was using the approach below.
[NSThread detachNewThreadSelector: #selector(spinBegin) toTarget:self withObject:nil];
from this link. As mentioned, it correctly spins the activity indicator on all except 4.*.. not sure why. To get around this, I also followed another approach something like (from developer.apple.com)
`
(IBAction)syncOnThreadAction:(id)sender
{
[self willStartJob];
[self performSelectorInBackground:
#selector(inThreadStartDoJob:)
withObject:theJobToDo
];
}
(void)inThreadStartDoJob:(id)theJobToDo
{
NSAutoreleasePool * pool;
NSString * status;
pool = [[NSAutoreleasePool alloc] init];
assert(pool != nil);
status = [... do long running job specified by theJobToDo ...]
[self performSelectorOnMainThread:
#selector(didStopJobWithStatus:)
withObject:status
waitUntilDone:NO
];
[pool drain];
}
`
The problem with this was that, it is showing the acitivityVIewIndicator spinning correctly (at least on the simulator) but after it stops, the built in activity indicator in the top bar (where it shows the battery% etc) is still spinning.
I'm new to objective C. I have finished my app completely but for this silly thing. I realize there is no way to display UIActivityView without starting another thread. and finally, just to rant, I don't understand why they have to make it so complicated. I mean they knew it was going to have this problem, why not provide a sample code everyone can use rather than deriving their own solutions.
Finally, can anyone please provide me with a direction or some sample code. I would really appreciate it. I have been searching for a few hours now and have not found anything really that works!
Why are you starting/stopping the indicator on a separate thread? Any methods you send to your UIActivityIndicatorView must be sent on the main (UI) thread.
Any events sent by a button pressed will automatically be run on the main thread. If you're using background threads to complete the process, you could do something like:
- (IBAction)buttonPressed:(id)sender {
// This runs on the main thread
[activityIndicator startAnimating];
[self performSelectorInBackground:#selector(inThreadStartDoJob:) withObject:theJobToDo];
}
- (void)inThreadStartDoJob:(id)theJobToDo {
// Set up autorelease pool
...
// Run your long-running action
...
// Stop the spinner. Since we're in a background thread,
// we need to push this to the UI Thread
[activityIndicator performSelectorOnMainThread:#selector(stopAnimating) withObject:nil waitUntilDone:YES];
}
Edit: As for the activity indicator in the top bar (where the battery is), doesn't this automatically start/stop based on network activity?
Currently I am retrieving a bunch of images from the internet and scaling them and then displaying them on my view.
The problem is I am doing it in the viewDidLoad method - so when the user taps they have to actually wait for this processing to happen and then the view is shown which causes a slight delay.
Is there anyway I could show the view and then somehow spark off the loading of the images AFTER the user has the view in front of them - similar to how a web page loads?
- (void)configureImages
{
if ([currentHotel.hotelImages count] > 0)
{
imageView1.image = [self getScaledImageFromURL: [currentHotel.hotelImages objectAtIndex:0]];
imageView2.image = [self getScaledImageFromURL: [currentHotel.hotelImages objectAtIndex:1]];
imageView3.image = [self getScaledImageFromURL: [currentHotel.hotelImages objectAtIndex:2]];
}
}
Consider NSOperation/NSOperationQueue, discussed in the Concurrency Programming Guide. There are several links to examples here.
Apps should use the asynchronous networking APIs for all networking to avoid blocking the user experience. It's best to avoid adding threads (such as happens when you use NSOperationQueue) for tasks like networking where the OS already provides async alternatives.
Apple supplies the async NSURLConnection, which works well but is a bit tedious to use. You can add a simple wrapper like gtm-http-fetcher which reduces async fetches to a single line with a callback when the load has finished. That will let you start all the loads in your viewDidLoad: method without stalling the user interface.
I am creating an iPhone application where I need to show a login screen for few minutes, hence I created the custom view and added to the custom view controller which is added to the window for display. Now at the same time I need to check for some background database so, I am creating that in separate delegate and while after database operation is in finished it gives an callback to the main thread to display the new screen. But the first view is never getting displayed and my application directly lands up in the new view.
Please find below my code snippet:
(void)CheckForExistingData : (DatabaseSource *)theDatabaseConnection
{
BOOL isRecordExist = theDatabaseConnection.isrecordExist;
// Release the connection....
[theDatabaseConnection release];
theDatabaseConnection = nil;
if (isRecordExist == FALSE)
{
textLabel.text = #"Preparing the application for first time use, please wait....";
[activityIndicator startAnimating];
[self setNeedsDisplay];
}
else
{
// Now all categories are successfully downloaded, launch the category screen...
sleep(2); // sleep for 1 second to allow to show the splash screen....
[self.viewController LaunchCategoryViewController:self];
}
}
Here CheckForExistingData is an callback mechanism which will be called from the other thread.
You need to exit your method to see anything displayed. Not sleep or wait on a synchronous network call.
That probably means you need to break your sequential code into multiple methods, the subsequent parts being called by a splash wait timer, the view button handler, or the async network activity completion callback.
sleep() blocks your main thread, thus the UI has no chance to update.
But you can always send messages delayed. In your case, it would look like this:
[self.viewController performSelector:#selector(LaunchCategoryViewController:) withObject:self afterDelay:2.0];
How can I run a clock that allows me to measure the time to load until appDidFinishLaunching ?
I want to set a sleep call that extends the Defaul.png show time to 3 seconds regardless the speed of the underlying hardware.
First off, you should know that Springboard in iPhone OS is kinda picky about load times. You should never make a sleep call somewhere in the loading process of you application. If Springboard detects that your application is taking too long to launch, your application will be terminated with "failed to launch in time" in the crash log.
Secondly, there is no, as far as I know, way of measuring the time your application took to launch. There are several thing happening when the user taps the Application icon on the springboard, and the iPhone OS provides no good information to your application.
One solution could be to make sure your applicationDidFinishLaunching: is very lightweight, and creating a "fake" Default.png overlay. By trimming down your applicationDidFinishLaunching: method to do only the most essential stuff, and the performing any time consuming tasks in the background, you can ensure that your Default.png overlay is displayed roughly the same time on different hardware.
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Create the image view posing with default.png on top of the application
UIImageView *defaultPNG = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Default.png"]];
// Add the image view top-most in the window
[window addSubview:defaultPNG];
[window makeKeyAndVisible];
// Begin doing the time consuming stuff in the background
[self performSelectorInBackground:#selector(loadStuff) withObject:nil];
// Remove the default.png after 3 seconds
[self performSelector:#selector(removeDefaultPNG:) withObject:defaultPNG afterDelay:3.0f];
}
- (void)removeDefaultPNG:(UIImageView *)defaultPNG {
// We're now assuming that the application is loaded underneath the defaultPNG overlay
// This might not be the case, so you can also check here to see if it's ok to remove the overlay
[defaultPNG removeFromSuperview];
[defaultPNG release];
}
If you add more views (your view controllers etc) in the loadStuff method, you should insert them below the defaultPNG overlay. You should also be aware of problems that could occur by doing these things from another thread. You could use performSelectorOnMainThread:withObject:waitUntilDone: if you encounter problems.