UIPageViewController delay page change animation - iphone

I am developing an application that makes use of a UIPageViewController. I noticed that if I change multiple pages too quickly, it causes several problems at runtime.
Is there a way to set a delay (such 2 or 3 milliseconds) between two page changes?
Thanks in advance.
************ DETAILED ANSWER **************
The solution is this:
-(void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed{
if(completed) {
[pageViewController.view setUserInteractionEnabled:NO];
[self performSelector:#selector(enableUserInteraction) withObject:nil afterDelay:0.2];
}
}
-(void)enableUserInteraction{
[self.view setUserInteractionEnabled:YES];
}

In your animation block, set userInteraction = NO until the animation finishes. This mean that the user will not be able to interact with the screen and thus change the page until it finishes animating.

I put it in the pageViewController:willTransitionToViewControllers: and used dispatch_after. With this solution the user cannot swipe quickly 2-3 times like in
pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray<UIViewController *> *)pendingViewControllers {
pageViewController.view.userInteractionEnabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
pageViewController.view.userInteractionEnabled = YES;
});
}

Related

How to Implement UIProgressView in iphone sdk

I have this code in a button click [NSThread detachNewThreadSelector: #selector(spinBegininapp) toTarget:self withObject:nil]; to show a activity indicator for user that the background thread is running ,i put this code to enable the activity-indicator
- (void)spinBegininapp
{
_activityindictor.hidden = NO;
}
and it works fine,when i click the button it shows the activity-indictor animating ,when the thread goes it hides the activity-indicator,but my need is to show a progressView instead of activity-indicator,it progresses according to the thread,and if the thread finishes it need to reach progress completely and self hide.is that possible .
#pragma mark - Loading Progress
static float progress = 0.0f;
-(IBAction)showWithProgress:(id)sender {
progress = 0.0f;
_progressView.progress = progress;
[self performSelector:#selector(increaseProgress) withObject:nil afterDelay:0.3];
}
-(void)increaseProgress {
progress+=0.1f;
_progressView.progress = progress;
if(progress < 1.0f)
[self performSelector:#selector(increaseProgress) withObject:nil afterDelay:0.3];
else
[self performSelector:#selector(dismiss) withObject:nil afterDelay:0.2f];
}
-(void)dismiss {
[self progressCompleted];
}
now call the following function whenever/where ever you want to show progress
[self showWithProgress:nil];
progress range lies between 0.0 and 1.0
1.0 means 100%
you can add a progress view no doubt but usually it used for definite quantities like time or data.. for eg. If you are downloading a 2mb file then you can always tell how much data you have downloaded and show the in the progress view as a factor. So if something similar is happening inside your thread you can use this..
UIProgressView *progressView = [[UIProgressView alloc] initWithProgressViewStyle:whateverStyle];
progressView.progress = 0.75f;
[self.view addSubview: progressView]
[progressView release];
you just need to update your progress as the value changes.... hoping this helps.

How do I run a process without blocking user interface in my iphone app

I am accessing the photo library on the iphone and it takes a long time to import the pictures i select in my application, how do i run the process on a secondary thread , or what solution do i use to not block the user interface?
I did a full explanation with sample code using performSelectOnBackground or GCD here:
GCD, Threads, Program Flow and UI Updating
Here's the sample code portion of that post (minus his specific problems:
performSelectorInBackground Sample:
In this snippet, I have a button which invokes the long running work, a status label, and I added a slider to show I can move the slider while the bg work is done.
// on click of button
- (IBAction)doWork:(id)sender
{
[[self feedbackLabel] setText:#"Working ..."];
[[self doWorkButton] setEnabled:NO];
[self performSelectorInBackground:#selector(performLongRunningWork:) withObject:nil];
}
- (void)performLongRunningWork:(id)obj
{
// simulate 5 seconds of work
// I added a slider to the form - I can slide it back and forth during the 5 sec.
sleep(5);
[self performSelectorOnMainThread:#selector(workDone:) withObject:nil waitUntilDone:YES];
}
- (void)workDone:(id)obj
{
[[self feedbackLabel] setText:#"Done ..."];
[[self doWorkButton] setEnabled:YES];
}
GCD Sample:
// on click of button
- (IBAction)doWork:(id)sender
{
[[self feedbackLabel] setText:#"Working ..."];
[[self doWorkButton] setEnabled:NO];
// async queue for bg work
// main queue for updating ui on main thread
dispatch_queue_t queue = dispatch_queue_create("com.sample", 0);
dispatch_queue_t main = dispatch_get_main_queue();
// do the long running work in bg async queue
// within that, call to update UI on main thread.
dispatch_async(queue,
^{
[self performLongRunningWork];
dispatch_async(main, ^{ [self workDone]; });
});
}
- (void)performLongRunningWork
{
// simulate 5 seconds of work
// I added a slider to the form - I can slide it back and forth during the 5 sec.
sleep(5);
}
- (void)workDone
{
[[self feedbackLabel] setText:#"Done ..."];
[[self doWorkButton] setEnabled:YES];
}
Use an asynchronous connection. It won't block the UI while it does the fetching behind.
THIS helped me a lot when I had to do download images, lot of them.
One option is use performSelectorInBackground:withObject:

how to display UIActivityIndicatorView BEFORE rotation begins

I'd like to display an activity indicator BEFORE the work undertaken by willAnimateRotationToInterfaceOrientation:duration: begins. Most of the time in my app, this work is quickly completed and there would be no need for an activity indicator, but occasionally (first rotation, i.e. before I have cached data, when working with a large file) there can be a noticeable delay. Rather than re-architect my app to cope with this uncommon case, I'd rather just show the UIActivityIndicatorView while the app generates a cache and updates the display.
The problem is (or seems to be) that the display is not updated between the willRotateToInterfaceOrientation:duration and the willAnimateRotationToInterfaceOrientation:duration: method. So asking iOS to show UIActivityIndicator view in willRotate method doesn't actually affect the display until after the willAnimateRotation method.
The following code illustrates the issue. When run, the activity indicator appears only very briefly and AFTER the simulateHardWorkNeededToGetDisplayInShapeBeforeRotation method has completed.
Am I missing something obvious? And if not, any smart ideas as to how I could work around this issue?
Update: While suggestions about farming the heavy lifting off to another thread etc. are generally helpful, in my particular case I kind of do want to block the main thread to do my lifting. In the app, I have a tableView all of whose heights need to be recalculated. When - which is not a very common use case or I wouldn't even be considering this approach - there are very many rows, all the new heights are calculated (and then cached) during a [tableView reloadData]. If I farm the lifting off and let the rotate proceed, then after the rotate and before the lifting, my tableView hasn't been re-loaded. In the portrait to landscape case, for example, it doesn't occupy the full width. Of course, there are other workarounds, e.g. building a tableView with just a few rows prior to the rotate and then reloading the real one over that etc.
Example code to illustrate the issue:
#implementation ActivityIndicatorViewController
#synthesize activityIndicatorView = _pgActivityIndicatorView;
#synthesize label = _pgLabel;
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);
}
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willRotate");
[self showActivityIndicatorView];
}
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willAnimateRotation");
[self simulateHardWorkNeededToGetDisplayInShapeBeforeRotation];
}
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
NSLog(#"didRotate");
[self hideActivityIndicatorView];
}
- (void) simulateHardWorkNeededToGetDisplayInShapeBeforeRotation;
{
NSLog(#"Starting simulated work");
NSDate* date = [NSDate date];
while (fabs([date timeIntervalSinceNow]) < 2.0)
{
//
}
NSLog(#"Finished simulated work");
}
- (void) showActivityIndicatorView;
{
NSLog(#"showActivity");
if (![self activityIndicatorView])
{
UIActivityIndicatorView* activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
[self setActivityIndicatorView:activityIndicatorView];
[[self activityIndicatorView] setCenter:[[self view] center]];
[[self activityIndicatorView] startAnimating];
[[self view] addSubview: [self activityIndicatorView]];
}
// in shipping code, an animation with delay would be used to ensure no indicator would show in the good cases
[[self activityIndicatorView] setHidden:NO];
}
- (void) hideActivityIndicatorView;
{
NSLog(#"hideActivity");
[[self activityIndicatorView] setHidden:YES];
}
- (void) dealloc;
{
[_pgActivityIndicatorView release];
[super dealloc];
}
- (void) viewDidLoad;
{
UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(50.0, 50.0, 0.0, 0.0)];
[label setText:#"Activity Indicator and Rotate"];
[label setTextAlignment: UITextAlignmentCenter];
[label sizeToFit];
[[self view] addSubview:label];
[self setLabel:label];
[label release];
}
#end
The app doesn't update the screen to show the UIActivityIndicatorView until the main run loop regains control. When a rotation event happens, the willRotate... and willAnimateRotation... methods are called in one pass through the main run loop. So you block on the hard work method before displaying the activity indicator.
To make this work, you need to push the hard work over to another thread. I would put the call to the hard work method in the willRotate... method. That method would call back to this view controller when the work is completed so the view can be updated. I would put show the activity indicator in the willAnimateRotation... method. I wouldn't bother with a didRotateFrom... method. I recommend reading the Threaded Programming Guide.
Edit in response to a comment: You can effectively block user interaction by having the willAnimateRotation... method put a non functioning interface on screen such as a view displaying a dark overlay over and the UIActivityIndicatorView. Then when the heavy lifting is done, this overlay is removed, and the interface becomes active again. Then the drawing code will have the opportunity to properly add and animate the activity indicator.
More digging (first in Matt Neuberg's Programming iPhone 4) and then this helpful question on forcing Core Animation to run its thread from stackoverflow and I have a solution that seems to be working well. Both Neuberg and Apple issue strong caution about this approach because of the potential for unwelcome side effects. In testing so far, it seems to be OK for my particular case.
Changing the code above as follows implements the change. The key addition is [CATransaction flush], forcing the UIActivityIndicatorView to start displaying even though the run loop won't be ended until after the willAnimateRotationToInterfaceOrientation:duration method completes.
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willRotate");
[self showActivityIndicatorView];
[CATransaction flush]; // this starts the animation right away, w/o waiting for end of the run loop
}
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;
{
NSLog(#"willAnimateRotation");
[self simulateHardWorkNeededToGetDisplayInShapeBeforeRotation];
[self hideActivityIndicatorView];
}
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
NSLog(#"didRotate");
}
Try performing you work on a second thread after showing the activity view.
[self showActivityIndicatorView];
[self performSelector:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil afterDelay:0.01];
Either execute the heavy lifting in a background thread and post the results in the foreground thread to update the UI (UIKit is only thread safe since iOS 4.0):
[self performSelectorInBackground:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil]
Or you can schedule the heavy lifting method to be executed after the rotation took place:
[self performSelector:#selector(simulateHardWorkNeededToGetDisplayInShapeBeforeRotation) withObject:nil afterDelay:0.4]
But these are only hacks and the real solution is to have proper background processing if your UI needs heavy processing to get updated, may it be in portrait or landscape. NSOperation and NSOperationQueue is a good place to start.

stopping a programming loop

When my main viewController is first clicked, it starts showing a demo (on repeat) as follows:
showingDemo = YES;
[self startDemo];
- (void)startDemo {
if (showingDemo) {
[self performSelector:#selector(stepone) withObject:nil afterDelay:1.5f];
[self performSelector:#selector(steptwo) withObject:nil afterDelay:2.0f];
[self performSelector:#selector(stepthree) withObject:nil afterDelay:3.8f];
[self performSelector:#selector(stepfour) withObject:nil afterDelay:4.3f];
[self performSelector:#selector(startDemo) withObject:nil afterDelay:5.6f];
}
}
When it is clicked a second time, I bring a new ViewController to the screen
showingDemo = NO;
[self.view addSubview:newView];
I thought this would stop the endless loop.
When the user returns back to my main viewController:
[newView.view removeFromSuperview];
And clicks on the screen again:
showingDemo = YES;
[self startDemo];
In testing my app, if I click back quickly (before the loop has had time to end, the program seems to be running through the loop twice - the one that was previously going and the new one - and therefore it looks all weird, with the 'stepthree' function happening before 'stepone' and so forth.
Anybody know a better way to STOP the loop I've programmed for good so that when I go to start it again later, it doesn't run multiple loops thinking that the previous one hasn't been finished?
Thanks so much!
When you set showingDemo to NO, call NSObject's cancelPreviousPerformRequestsWithTarget: to cancel any pending perform requests:
showingDemo = NO;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self.view addSubview:newView];

Add view overlay to iPhone app

I'm trying to do something like this:
- (void)sectionChanged:(id)sender {
[self.view addSubview:loadingView];
// Something slow
[loadingView removeFromSuperview];
}
where loadingView is a semi-transparent view with a UIActivityIndicatorView. However, it seems like added subview changes don't take effect until the end of this method, so the view is removed before it becomes visible. If I remove the removeFromSuperview statement, the view shows up properly after the slow processing is done and is never removed. Is there any way to get around this?
Run your slow process in a background thread:
- (void)startBackgroundTask {
[self.view addSubview:loadingView];
[NSThread detachNewThreadSelector:#selector(backgroundTask) toTarget:self withObject:nil];
}
- (void)backgroundTask {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
// do the background task
[self performSelectorOnMainThread:#selector(backgroundTaskDone) withObject:nil waitUntilDone:NO];
[pool release];
}
- (void)backgroundTaskDone {
[loadingView removeFromSuperview];
}
Two potential problems spring to mind, both centred around how you've implemented the 'do something slow here' code.
First off, if it's locking up the main thread then it's possible the application's UI isn't being redrawn in time to display the view, i.e. Add Subview, tight loop/intensive processing tying up the main thread, then immediately after the view is removed.
Secondly if the 'something slow' is being done asynchronously, then the view is being removed while the slow processing is running.
One things for sure, your requirements are as follows:
Add a subview to display some kind of 'loading' view
Invoke a slow running piece of functionality
Once the slow running functionality completes, remove the 'loading' subview.
- (void)beginProcessing {
[self.view addSubview:loadingView];
[NSThread detachNewThreadSelector:#selector(process) toTarget:self withObject:nil];
}
- (void)process {
// Do all your processing here.
[self performSelectorOnMainThread:#selector(processingComplete) withObject:nil waitUntilDone:NO];
}
- (void)processingComplete {
[loadingView removeFromSuperview];
}
You could also achieve something similar with NSOperations.