my main UIViewController should check for some data updates when it loads, if any update is found an opaque subview is added to show progress and so on.
Let's say that the method is called checkUpdates, so in my app delegate i do the following:
[window addSubview:viewController.view];
[window makeKeyAndVisible];
[viewController checkUpdates];
The method is like
- (void) checkUpdates {
// add the opaque progress view
[self.view addSubview:progress.view];
[progress setMessage: #"Checking for updates ..."];
// ... perform the update ...
// remote the progress view
[progress.view removeFromSuperview];
}
Theoretically the view should load and the update process should be seen, but the problem is that the view just get freezed during this phase, and when the update process is finished i'm able to interact with it.
Any idea?
Since the UI runs on the main thread of the app, if you perform "work" tasks on the main thread, you will also block the UI, which will therefore look frozen.
You might want to consider using NSOperationQueue or something equivalent to spin off another thread to do your work so the UI can continue to process updates independently.
Related
Is this code using UIActivityIndicatorView flawed? It appears that I don't actually get to see the indicator/spinner at all here, so is this because the view isn't drawn until the who viewDidLoad completes?
Is the only way around this to do the viewDidLoad custom work (e.g. data updates) on a separate thread? (I was hoping in this case for an easier single-thread operation). Is there a way to force the view to refresh after the "startAnimating" line perhaps prior to the data loading commencment?
Code from UITableViewController implementation:
- (void)viewDidLoad {
// Wait indicator - Start
self.waitView = [[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge] autorelease];
self.waitView.hidesWhenStopped = true;
[self.view addSubview: self.waitView];
// Load data into tableview
[NSThread sleepForTimeInterval: 5.0]; // Test code to simulate
[self.waitView stopAnimating];
}
You should also call startAnimating. Sleeping is not a good idea. I would prefer the performSelector-methods which starts a not recurring NSTimer under the hood.
Try this:
-(void) doStuff:(id)aSender
{
[self.waitView stopAnimating];
}
-(void)viewDidLoad
{
...
[self performSelector:#selector(doStuff:) withObject:self afterDelay:5.0];
}
in addtion: also set the frame- or bounds-property of the ActivityIndicatorView somewhere like sosborn said in his comment
Actually the answer from Thomas should work as it is, I will add a little explanation as to why not use sleep as you have done it.
All the UI processing on iPhone (and most of OSs as well) is being done in only one thread - the main thread, the thread that executes the so called run loop. If you stop that thread the UI will stop, nothing will be drawn.
Putting sleep into viewDidLoad, which runs in the main thread, will do just that - stop UI from doing anything. So because immediately after wakeup you've called [self.waitView stopAnimating] and the activityview should hide when not animating, you can't see it at all - you just didn't give it any time to show.
Thomas used a NSTimer to call stopAnimating after 5 seconds - now this lets the main thread to execute code before stopping animation and hiding waitView and this will work for your test.
Better yet you just let it animate without any timer and use a delegate patter to be informed by the tableView loading code after the data has been loaded, then stop animating. You don't know how long loading of data will last, so it's better to wait until it's finished than stop animating after any specific time.
Oh well, and the size and position, makes sense, but for testing it doesn't matter and is not the cause of not seeing it - if not specified it will be added at 0,0 and have a default size so you will see it anyway.
From what I understand any new view that is opened needs to have a parent/super view.
I have a background thread that communicates with a server and according to the server's response it needs to popup an alert and in response to that alert it needs to bring up some UI. This can happen 'anywhere' within my App and so I find it hard to provide this new view with a parent/super view.
Is it possible to open this new view with no knowledge of what's currently being displayed? Is there a non-intrusive way to detect the current view and use that as the parent/super?
One of the targets is not to require anything from the Delegate and other views.
The way to get the currently visible view depends on the setup of the controllers, eg. whether you use a tab bar, navigation controller etc. I think the easiest way would be if your background thread sent a notification (using NSNotificationCenter) on the main thread whenever you need the UI changes to happen. The View Controllers can then subscribe to this notification and handle the UI changes.
Notifications are just one way of communication though, basically what you want is to have your model background thread somehow message the currently active view controller on your main thread. How you exactly go about this will largely depend on your application (see first sentence again).
Actually you don't need a view, you can add it as a subview of your app delegate's window.
In your application delegate:
- (void) showView {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0,0,320,480)];
view.backgroundColor = [UIColor redColor ];
[self.window addSubview:view];
[view release];
}
Then, when you want to show the view:
MyAppDelegate *d = [[UIApplication sharedApplication] delegate];
[d showView];
You'll need a way to dismiss the view once you are done with it, you can use
[self removeFromSuperview]
I have an app that starts with a tableview (from a xib) that loads multiple navigation controllers. I want to present a modal view on startup if a long init sequence is underway. I tried presenting a modal view in the App Delegate, but the view doesn't appear until well after the underlying code is already complete.
MainWindow.xib loads TableViewController, so I put my call to presentmodalview in that View Will Appear with the same result. I have numerous NSLOG calls so I can watch what is happening but I can't figure out why the view doesn't appear until after both the app delegate and the tableview controller's viewWillAppear finish. I moved the call to viewDidAppear with the same result. Here is a code snippet:
App Delegate:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
// Add the tab bar controller's current view as a subview of the window
[window addSubview:tabBarController.view];
[window makeKeyAndVisible];
[[UIApplication sharedApplication] setStatusBarHidden:NO];
// if new version being installed do init stuff
if ( <needs update code here>) {
Uncompress *uncompressView = [[Uncompress alloc] initWithNibName:#"Uncompress" bundle:nil];
[self.tabBarController presentModalViewController:uncompressView animated:NO];
[uncompressView release];
}
}
I also tried changing presentmodalviewcontroller to [window addSubview:uncompressView.view] without any luck.
The Update code runs just fine, the problem is the view doesn't appear until both the AppDelegate and the TableView are finished. I'm not creating any views programatically - all are from Xib's. I can't figure out where to call the update function to get the view to appear right away. Any help appreciated. Thank you!
On iOS, the UI is only updated when your code returns control to the run loop. So if your uncompress task takes a lot of time, the UI will only be updated after it has finished.
You can sort of work around this issue by putting the time-intensive task in a separate method and calling that method with [self performSelector:... afterDelay:0.0 ...]; but it is not a good solution because even if the UI will update, user interaction with your app will still be blocked as long as your code blocks the main thread. And if your code takes more than a few seconds to run (e.g. because it runs on an older, slower device), the OS watchdog timer will kill your app.
The best solution would be to put the time-intensive task in a background thread, e.g. with NSOperation or dispatch_async().
My understanding of Runloops is basic so this may seem like a very trite question.
I have the following in my application:didFinishLaunchingWithOptions (or applicationDidFinishLaunching):
{
// 1. typical app setup work: create various views, create a tab bar, add
// navigation controller and views to the tab bar
// 2. perform some other app initialization tasks
// 3. Add main view to the window
[window addSubview:tabbarController.view];
// 4. Make window visible
[window makeKeyAndVisible];
// 5. Perform one final behind the scene task
[myTaskObject doSomeTaskHere];
}
Do each of these methods get executed in order listed or is there any chance that step #5 can happen before the app's main runloop completes the work of putting up the main window with '[window makeKeyAndVisible]'
Does the doSomeTaskHere need to get wrapped up into a performSelectorOnMainThread:withObject:waitUntilDone:YES to ensure that the runloop completes the displaying of the window and thus loading whatever view that is the topmost view before 'doSomeTaskHere' is invoked?
Those tasks will execute in order on the main thread's run loop. Since UI updates also occur on the main thread you will not allow your app to redraw the screen until after you return from -application:didFinishLaunchingWithOptions: so while [window makeKeyAndVisible]; will complete before [myTaskObject doSomeTaskHere]; you are still blocking the UI from updating until that doSomeTaskHere is complete.
If doSomeTaskHere is an expensive operation you should schedule it for a future iteration of the run loop or better yet do that work on a different thread so that the UI can update and respond to touches.
performSelectorOnMainThread:withObject:waitUntilDone:YES would not allow the main thread to update the UI as unless you passed NO as the last parameter. Telling the main thread to wait until the main thread finishes some other work isn't very useful or any different than invoking that selector directly.
How do I create a loading screen that can be reused at any given time. I'm aware of the Default.png but I need the flexibility to plug in a loading screen at any point during the application life cycle.
This is what I have thus far.
//inside a method that gets called by a UIButton
LoadingViewController* loadController = [[LoadingViewController alloc] initWithNibName:#"Loading" bundle:nil vertical:NO];
[self.view addSubview: loadController.view];
//some method call that takes a few seconds to execute
[self doSomething];
//This loads some other view, my final view
[self.view addSubview: someOtherView]
but it seems that the loading view is never displayed. Instead the previous view stays there until the "someOtherView" gets added. I put trace logs and the code does seem to get executed, I even replaced [self doSomething] with a sleep(2), but the intermediate loading view is never displayed.
If I remove [self.view addSubview:someOtherView]; then after a few seconds...(after doSomething finishes executing) the load view is displayed since there is no view that is pushed on top of it, however this is obviously not the functionality I want.
Can explain this behavior? Is there something about the rendering cycle that I am misunderstanding because it doesn't seem like the view (on the screen at least) is instantly updated, even though I call a [self.view addSubview: loadController.view];
Would I need to create a separate thread?
In general, for changes in the UI to be made visible to the user, control must return to the main runLoop. You are only returning to the runLoop after taking the loading view down and replacing it with the other view. One strategy for dealing with this is to move the code that does the loading onto another thread. NSOperation and NSOperationQueue can be used for this.
An even simpler approach is to use performSelectorInBackground:withObject to do the processing. Once processing is complete the UI can be updated again to show the data. It is important to remember that the UI updates must be carried out on the main thread. Use performSelectorOnMainThread:withObject:waitUntilDone: to accomplish this from the loading thread.
This sounds like a lot of complication but it is really as simple as breaking your single method up into three separate methods as follows:
Display the loading view and start the background process - this is the button action method.
Do the background loading - called from the button action function with performSelectorInBackground:withObject.
Remove the loading view and update the display with the data - called from the background thread with performSelectorOnMainThread:withObject:waitUntilDone.
I created a subclass of UIView where I initialized how my loading-view should work and look like. (My view appeared and slided in from the bottom with an nice animation).
I then added code that handled whether the loading-view should be visible or not in a subclass of UIViewController.
I then let all my viewcontrollers be an subclass of my new viewcontrollerclass which made it possible for me to do:
[self showloadingMessage:#"loading..."];
in all my viewcontrollers...