When programming for iOS, I frequently find myself faced with the following situation:
- (void)someMethod
{
[self performSomeAnimation];
//below is an action I want to perform, but I want to perform it AFTER the animation
[self someAction];
}
- (void)performSomeAnimation
{
[UIView animateWithDuration:.5 animations:^
{
//some animation here
}];
}
Faced with this situation, I usually end up just copy/pasting my animation code so that I can use the completion block handler, like so:
- (void)someMethod
{
[self performSomeAnimation];
//copy pasted animation... bleh
[UIView animateWithDuration:.5 animations:^
{
//same animation here... code duplication, bad.
}
completion^(BOOL finished)
{
[self someAction];
}];
}
- (void)performSomeAnimation
{
[UIView animateWithDuration:.5 animations:^
{
//some animation here
}];
}
What is the proper way to solve this problem? Should I be passing a block of code to my -(void)performSomeAction method, like below, and executing that block on completion of the animation?
- (void)someMethod
{
block_t animationCompletionBlock^{
[self someAction];
};
[self performSomeAnimation:animationCompletionBlock];
}
- (void)performSomeAnimation:(block_t)animationCompletionBlock
{
[UIView animateWithDuration:.5 animations:^
{
//some animation here
}
completion^(BOOL finished)
{
animationCompletionBlock();
}];
}
Is that the proper way to solve this problem? I guess I have been avoiding it because I'm not THAT familiar with block usage (not even sure if I declared that block properly) and it seems like a complicated solution to a simple problem.
You could also do this:
- (void)performSomeAnimationWithCompletion:(void(^)(void))animationCompletionBlock
{
[UIView animateWithDuration:.5 animations:^
{
//some animation here
}
completion^(BOOL finished)
{
animationCompletionBlock();
}];
}
And instead of explicitly defining a block and passing it as parameter, you can call it directly like this (this is how block animations work for UIView, for example):
- (void)someMethod
{
[self performSomeAnimationWithCompletion:^{
[self someAction];
}];
}
From what I can understand, it seems that you already pretty much have the answer, you just need to delete the first call to performSomeOperation :
- (void)someMethod
{
[UIView animateWithDuration:.5 animations:^
{
//Your animation block here
}
completion: ^(BOOL finished)
{
//Your completion block here
[self someAction];
}];
}
Related
I'm creating a new UIViewController but waiting a few seconds before I'm showing it.
The thing is I want viewDidLoad to be called before I'm actually showing the now object. How can I make viewDidLoad run?
Currently I'm doing something like this:
UIView *tempView = newObject.view;
But I really don't like that solution.
If there is code that you want to run that you want to be called when the view is instantiated then just put it in the init method.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName: nibNameOrNil
bundle: nibBundleOrNil];
if (self)
{
// Custom stuff
}
return self;
}
Is there another reason why you are wanting viewDidLoad to be called?
Looks like just calling [newObject view]; does the trick!
- (void) viewDidLoad {
[super viewDidLoad];
self.view.alpha = 0; //When alpha = 0, it is also hidden
[UIView animateWithDuration:2.0f animations:^{
// Nothing to animate here
} completion:^(BOOL finished) {
// On animation completion show the content of the view, also animated.
[UIView animateWithDuration:0.5f animations:^{
self.view.alpha = 1;
}];
}];
}
The viewDidLoad is always called before the object is shown in screen.
If you want some delay , you can set some delay code inside viewDidLoad or viewDidAppear. But why exactly do you need the delay , there may be other solutions to solve your issue.
I'm delaying UIKit messages as per this SO answer
Now another requirement has arisen, instead of just queuing SSHUDView method calls we should also handle queing of UIAlertView. For example one scenario could be that we display a hud then after 1 second we display another hud and then finally after 1 second we display a UIAlertView.
The problem is now that since the SSHUDViews are run asynchronously on a background thread when I get to display the UIAlertView the SSHUDViews have not finished displaying so the UIAlertView will overlay the hud.
Basically I need a way to queue up and delay methods whether they are of class SSHUDView or UIAlertView. A feedback queue, where you can delay individual messages.
What you're talking about sounds like a perfect fit for semaphores (see under the heading Using Dispatch Semaphores to Regulate the Use of Finite Resources)! I saw the SO Answer you linked to and I don't think it solves the case of UIView animations. Here's how I'd use semaphores.
In your view controller add an instance variable dispatch_semaphore_t _animationSemaphore; and initialize it in the - init method:
- (id)init
{
if ((self = [super init])) {
_animationSemaphore = dispatch_semaphore_create(1);
}
return self;
}
(Don't forget to release the semaphore in the - dealloc method using dispatch_release. You also might want to wait for queued animations to finish by using dispatch_semaphore_wait, but I'll leave that for you to figure out.)
When you want to queue up an animation, you'll do something like this:
- (void)animateSomething
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
dispatch_semaphore_wait(_animationSemaphore, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.5 animations:^{
// Your fancy animation code
} completion:^(BOOL finished) {
dispatch_semaphore_signal(_animationSemaphore);
}];
});
});
}
You can use the - animateSomething template to accomplish different things, such as displaying an SSHUDView or a UIAlertView.
What you're describing sounds like an animation. Why not just use UIView animation and chain the series of animation blocks:
[UIView animateWithDuration:2
animations:^{
// display first HUD
}
completion:^(BOOL finished){
[UIView animateWithDuration:2
animations:^{
// hide first HUD, display second HUD
}
completion:^(BOOL finished){
[UIView animateWithDuration:2
animations:^{
// hide second HUD, show UIAlert
}
completion:nil
];
}
];
}
];
I have several dispatch_async methods linked so they authenticate a user in my system.
The next async method only happens when the previous one finishes, as they each have completion handlers.
When the last one is finished I perform a custom segue with 2 uiview animation blocks.
However, when I log when each of these actually run, there is a considerable gap between the log and the animation actually happening, eventually the views animate and the completion block is called.
I don't really know how useful it would be adding my code here, but i have tested and it must be the async methods because if I comment them out and just return YES the animations happen without delay at the same time as the log.
Does anyone know why this might happen?
EDIT *(with code)
Typical 'exist check' used for email, user, user id.
- (void)existsInSystemWithCompletionHandler:(void (^)(BOOL))block
{
self.existsInSystem = NO;
if (self.isValid) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//
// Get data
//
if (dataIsValid) {
block(YES);
} else {
block(NO);
}
});
} else {
block(self.existsInSystem);
}
}
Check user exists
[potentialUser existsInSystemWithCompletionHandler:^(BOOL success) {
if (success) {
// Perform segue
//
[self performSegueWithIdentifier:#"Logging In" sender:self];
}
}];
Segue
- (void)perform
{
NSLog(#"Perform");
LogInViewController *sourceViewController = (LogInViewController *)self.sourceViewController;
LoggingInViewController *destinationViewController = (LoggingInViewController *)self.destinationViewController;
destinationViewController.user = sourceViewController.potentialUser;
// Animate
//
[UIView animateWithDuration:0.2f
animations:^{
NSLog(#"Animation 1");
//
// Animate blah blah blah
//
}];
[UIView animateWithDuration:0.4f
delay:0.0f
options:UIViewAnimationOptionCurveEaseIn
animations:^{
NSLog(#"Animation 2");
//
// Animate blah blah blah
//
}
completion:^(BOOL finished) {
NSLog(#"Completion");
//
// Finished
//
[sourceViewController presentViewController:destinationViewController animated:NO completion:nil];
}];
}
LoggingIn VC
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.user loginWithCompletionHandler:^(BOOL success) {
if (success) {
[self performSegueWithIdentifier:#"Logged In" sender:self];
}
}];
}
Fixed! It seems to work now that in my code I have added the lines:
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock(success);
});
From your description it seems likely that your segue is waiting for a process to finish.
Maybe your nested async methods that follow each other through their completion methods have produced somewhat convoluted code. This might be the reason why you could be overlooking the blocking method.
One way to tidy up your code is use a sequential queue. The blocks pushed to the queue will start only when the previous one has finished.
I am trying to trigger an animation in one view, based on what is happening in a separate class file. It gets to the method, supported by the fact that it does spit out the two NSLog statements, but doesn't commit the UIView Animation
Here is the code:
ViewController.h
#interface ViewController : UIViewController {
}
-(void)closeMenu;
ViewController.m
-(void)closeMenu{
//close the menu
NSLog(#"Got here");
[UIView animateWithDuration:0.6 animations:^{
[UIView setAnimationBeginsFromCurrentState:YES]; // so it doesn't cut randomly, begins from where it is
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[menuView setFrame:CGRectMake(menuView.frame.origin.x, -menuView.frame.size.height, menuView.frame.size.width, menuView.frame.size.height)];
}];
NSLog(#"Got here2");
}
OtherClass.m (commented code may be irrelevant to this question, of course still used in actual application. Jut thought it might make it easier for comprehension)
#import "ViewController.h"
...
//- (void) item:(SDGroupCell *)item subItemDidChange:(SDSelectableCell *)subItem
//{
ViewController *foo = [[[ViewController alloc] init] autorelease];
//SelectableCellState state = subItem.selectableCellState;
//NSIndexPath *indexPath = [item.subTable indexPathForCell:subItem];
//switch (state) {
//case Checked:
//NSLog(#"Changed Sub Item at indexPath:%# to state \"Checked\"", indexPath);
//close the menuView
[foo closeMenu];
//break;
//case Unchecked:
//NSLog(#"Changed Sub Item at indexPath:%# to state \"Unchecked\"", indexPath);
//break;
//default:
//break;
//}
}
You're mixing old fashioned animation, with block based animation. For example, in the documentation, it states for setAnimationBeginsFromCurrentState:
Use of this method is discouraged in iOS 4.0 and later. Instead, you
should use theanimateWithDuration:delay:options:animations:completion:
method to specify your animations and the animation options.
I'm not 100% sure if this is supported. You should change your animation code to this at least:
[UIView animateWithDuration:0.6
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionCurveEaseInOut
animations:^{
[menuView setFrame:CGRectMake(menuView.frame.origin.x, -menuView.frame.size.height, menuView.frame.size.width, menuView.frame.size.height)];
}
completion:nil];
Apart from that, it seems like it should work. It may cause issues if anything else if affecting the frame. It may be worth calculating the frame before the animation block. A'la:
CGRect newFrame = CGRectMake(menuView.frame.origin.x, -menuView.frame.size.height, menuView.frame.size.width, menuView.frame.size.height)
[UIView animateWithDuration...:^{ menuView.frame = newFrame; }...];
EDIT: Oh wait, looks like you're alloc/init'ing the object in (void) item:(SDGroupCell *)item subItemDidChange:(SDSelectableCell *)subItem, and calling the method on it, but the view is nowhere in the view hierarchy. You need to call the animation on an instance which is being displayed on screen. Hope that makes sense?
EDIT 2: To call it on an instance which is already being displayed, typically you need to store it in an instance variable. I can't say exactly how in your situation, but generally it'd be of the form:
#interface OtherClass () {
ViewController* m_viewController;
}
#end
....
- (void)viewDidLoad // Or where ever you first create your view controller
{
...
// If you're adding a ViewController within another ViewController, you probably need View Controller Containment
m_viewController = [[ViewController alloc] init];
[self addChildViewController:m_viewController];
[m_viewController didMoveToParentViewController:self];
[self.view addSubview:m_viewController.view];
...
}
// If you're using ARC code, the dealloc wouldn't typically be necessary
- (void)dealloc
{
[m_viewController release];
[super dealloc];
}
//- (void) item:(SDGroupCell *)item subItemDidChange:(SDSelectableCell *)subItem
//{
//SelectableCellState state = subItem.selectableCellState;
//NSIndexPath *indexPath = [item.subTable indexPathForCell:subItem];
//switch (state) {
//case Checked:
//NSLog(#"Changed Sub Item at indexPath:%# to state \"Checked\"", indexPath);
//close the menuView
[m_viewController closeMenu];
//break;
//case Unchecked:
//NSLog(#"Changed Sub Item at indexPath:%# to state \"Unchecked\"", indexPath);
//break;
//default:
//break;
//}
}
If you need to access it from outside the class, this won't be sufficient, use properties for that. I.e.
Header File
#property (nonatomic, strong) ViewController* myViewController
.m file
// Use it as such
[self.myViewController closeMenu];
That animation code is really strange. You are mixing the new and the old UIView animation code and I don't think you can do that (but I could be wrong).
Since your have begun using the block based API I would recommend going that route (Apple recommends the same thing).
There is a similar method to the one you've used that takes options called animateWithDuration:delay:options:animations:completion:. You can pass in 0 for the delay and an empty block that takes a BOOL for the completion.
The two flags you want to pass for the options are UIViewAnimationOptionBeginFromCurrentState and UIViewAnimationOptionCurveEaseInOut.
Your code would look something like this
[UIView animateWithDuration:0.6
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut
animations:^{
// set your frame here...
[menuView setFrame:CGRectMake(menuView.frame.origin.x,
-menuView.frame.size.height,
menuView.frame.size.width,
menuView.frame.size.height)];
} completion:^(BOOL finished){}];
This is very strange. My view is receiving a notification with an index number that I use to find a UIbutton by tag and then change its text label. The only trouble is the text will not change. I've put in break points and the code is being executed.
Is there some peculiarity with variable scopes when using notifications?
- (void)receiveEvent:(NSNotification *)notification {
int pass = [[[notification userInfo] valueForKey:#"index"] intValue];
NSLog(#"button index is %i", pass);
UIButton *changebut = (UIButton *) [self.view viewWithTag:pass];
ButtonStatusModel *m= [self.buttoneventpendingarray objectAtIndex:pass];
NSLog(#"current but status = %i",m.btnstatus);
if (m.btnstatus==3 ) {
//this should change the label but it does not;
[changebut setTitle:#"playing" forState:UIControlStateNormal];
m.btnstatus=2;
NSLog(#"should set text to 'playing");
//[engine addActionToQue:buttonid actionSample:actionsample action:1];
}else if (m.btnstatus==1) {
//this should change the label but it does not;
[changebut setTitle:#"idle" forState:UIControlStateNormal];
NSLog(#"should set text to 'idle");
m.btnstatus=0;
//[engine addActionToQue:buttonid actionSample:actionsample action:0];
}
}
EDIT 1
Thanks to commenters I've determined that even though my reciveevent function in on my main view controller the function is not being executed from the main thread (dont really understand this) and so thats why the button label will not change.
I used the following code to determine if it was the main thread or not. I think now to change the button label I need to call perform selector on main thread? Is this the correct thing to do? If so I need to pass a function 2 variables, I dont see how perform selector on main thread accomodates this.
-(void) updatebutton:(int)tag changeto:(int) val
{
if ([NSThread isMainThread]) {
{
NSLog(#"is main thread");
}
} else {
NSLog(#"is not thread");
[self performSelectorOnMainThread:#selector( /*function with 2 parameters here*/) withObject:nil waitUntilDone:YES];
}
}
EDIT 3*
Using blocks instead of main selector helped me out of my problem.
I really don't understand the whole main thread though. I assumed that if code was in my view controller that any executed code would be performed on the main thread. That seems to be big fat wrong. I would appreciate a little enlightenment on the matter.
Heres what I used anyway
-(void) updatebutton:(int)tag changeto:(int) val
{
UIButton *changebut = (UIButton *) [self.view viewWithTag:tag];
if ([NSThread isMainThread]) {
{
NSLog(#"is main thread");
if (val==2) {
[changebut setTitle:#"playing" forState:UIControlStateNormal];
}
if (val==0) {
[changebut setTitle:#"idle" forState:UIControlStateNormal];
}
}
} else {
NSLog(#"is not thread");
//[self performSelectorOnMainThread:#selector(updatebutton::) withObject:nil waitUntilDone:YES];
if (val==2) {
dispatch_async(dispatch_get_main_queue(), ^{
[changebut setTitle:#"playing" forState:UIControlStateNormal];
});
}
if (val==0) {
dispatch_async(dispatch_get_main_queue(), ^{
[changebut setTitle:#"idle" forState:UIControlStateNormal];
});
}
}
}
Try putting
[changebut setTitle:#"idle" forState:UIControlStateNormal];
in the -viewDidAppear method. If it doesn't work either, make sure that all the outlets are connected in IB. Moreover, try adding a few spaces as the button title in your .xib file (if you have one).
Is -receiveEvent called on another than the main thread? UI changes must be performed on the main thread.