I have a problem with an animation.
The problem is that if I try to animate a view that is already created all goes well, if I try to create and animate a view at the same time the animation doesn't work.
Can anyone help me?
My Methods
+ (LoginView *)sharedInstance {
#synchronized(self) {
if (nil == _sharedInstance) {
_sharedInstance = (LoginView *)[[[NSBundle mainBundle] loadNibNamed:#"LoginView" owner:nil options:nil] objectAtIndex:0];
}
}
return _sharedInstance;
}
- (void)hide:(BOOL)value animated:(BOOL)animated {
CATransition * animation = [CATransition animation];
animation.type = kCATransitionFade;
[animation setDuration:1.0];
if(_autoManageModalView)
[animation setDelegate:self];
[[self layer] removeAllAnimations];
[[self layer] addAnimation:animation forKey:kCATransition];
self.hidden = value;
}
How I call them
[[LoginView sharedInstance] hide:NO animated:YES];
The first time (with the same call) animation doesn't work, from the secondo time all goes well. Thank in advance!
You are animating your view too early in its lifecycle. In theory, you create a view, then display it somewhere (e.g., addSubview:), then you animate it.
It is highly possible, though I have not checked it, that the first time that your hide:animated: method is called the self.layer property is null; in any case, the animation would happen before the view is displayed, so you would not see it.
All in all, first display the view, then call the hide:animated: method on it.
After your comment: try and call the hide:animated: method through a method like:
performSelector:withObject:afterDelay:
If you specify a 0.0 delay, this will simply queue the call to hide:animate: on the main loop, so that all the processing related to loadNibNamed: can happen and so give your view the time to be set up for display correctly.
In order to use performSelector:withObject:afterDelay: you will need to modify your method signature so that it takes one argument and this must be an NSObject-derived type, not a primitive type.
Related
I have a UIButton subclass that does some custom drawing and animations. That is all working fine and dandy.
However, most of my buttons dismiss the current view via their superview calling [self dismissViewControllerAnimated] once it is confirmed with the model that whatever the button push was supposed to accomplish was actually done, and I want there to be a delay to allow the animation to complete before dismissing the view.
I am able to easily enough animate the UIButton subclass on touchesEnded and then call [super touchesEnded], which works fine except that it doesn't let my animations finish before dismissing the view. Like this:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"transform.foo"];
//set up myAnimation's properties
[self.layer addAnimation:shakeAnimation forKey:nil];
[super touchesEnded:touches withEvent:event]; //works! but no delay
}
My first attempt at creating a delay was by using CATransaction, as follows:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"transform.foo"];
//set up myAnimation's properties
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[super touchesEnded:touches withEvent:event]; //doesn't seem to do anything :-/
}];
[self.layer addAnimation:shakeAnimation forKey:nil];
[CATransaction commit];
}
Which, as far as I can tell, is executing CATransaction's completionBlock, but it just isn't doing anything.
I've also tried assigning the touches and event arguments from touchesEnded to both properties and global variables, and then executing [super touchesEnded] in another method called by an NSTimer. The same thing seems to be occurring where the code is executing, but my call to [super touchesEnded] isn't doing anything.
I've dug around online for hours. Added stubs of the other touches methods from UIResponder which just contain [super touches...]. Tried setting up my global variables for the method called by NSTimer differently (I very well may be missing something in regards to global variables...). This button is being created by the Storyboard, but I've set the class to my custom class, so I don't think UIButton's +(UIButton *)buttonWithType method is affecting this.
What am I missing? Is there some small thing I'm forgetting about or is there just no way to delay the call to [super touchesEnded] from a UIButton subclass?
I was not able to solve this, only find a way around it.
My last stab at solving this was to figure out whether the [super touchesEnded...] inside the completion block was being executed in a thread that was different from the thread it was executed in when it was outside the completion block... and no, they both appear to be the main thread (Apple's documentation on CATransaction does state that its completionBlock is always run in the main thread).
So in case someone else is banging their head against this, here's my less-than-elegant solution:
1.) In my subclass of UIButton I created a weak property called containingVC.
2.) In every single (ugh) VC that uses the custom button class I have to do this:
#implemenation VCThatUsesCustomButtonsOneOfWayTooMany
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.firstCustomButton.containingVC = self;
self.secondCustomButton.containingVC = self;
self.thirdCustomButton.containingVC = self;
....
self.lastCustomButton.containingVC = self;
//you're probably better off using an IBOutletColletion and NSArray's makeObjectPerformSelector:withObject...
}
#end
3.) Then in my custom UIButton class I have something like this:
-(void)animateForPushDismissCurrView
{
CAAnimation *buttonAnimation = ...make animation
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[self.containingVC performSegueWithIdentifier:#"segueID" sender:self.containingVC];
}];
[self.layer addAnimation:buttonAnimation forKey:nil];
[CATransaction commit];
}
4.) Then in whatever VC that the user is currently interacting with, after making sure the button has done whatever it was supposed to do (in my case it checks with the model to confirm that the relevant change was made), each button has to call [someCustomButton animateForPushDismissCurrView] which then animates the button press and then fires the UIStoryboardSegue that actually dismisses the view.
Obviously this would work for going deeper, not just unwinding, but you'd need additional logic in the custom button's -(void)animateForPush method or a separate method entirely.
Again, if I'm missing something here, I'd love to know what it is. This seems like an absurd number of hoops to jump through to accomplish what seems like a simple task.
Lastly, and most importantly, if it just won't work with the [super touchesEnded...] method in CATransaction's completionBlock, I'd like to know WHY. I suspect that it has something to do with threads or the weirdness that is Objective-C's super keyword.
When I give a good swipe to my tableView and press the "Back" button before the tableView ended it's scrolling, my app crashes. I've tried the following:
- (void) closeViewController
{
[self killScroll];
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)killScroll
{
CGPoint offset = sellersTableView.contentOffset;
[sellersTableView setContentOffset:offset animated:NO];
}
That didn't work, same crash. I don't see why, the error I'm getting is the following:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:'
So that means that the tableView is still requesting a cell when everything is already being deallocated. Makes no sense.
Then I tried this:
- (void) closeViewController
{
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dealloc
{
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
sellersTableView = nil;
}
Gives me the same error. Any ideas?
Update:
My delegate methods
creation
if (textField == addSellerTextField) {
sellersTableView = [[UITableView alloc] initWithFrame:CGRectMake(addSellerTextField.frame.origin.x + addSellerTextField.frame.size.width + 10, addSellerTextField.frame.origin.y - [self heightForTableView] + 35, 200, [self heightForTableView])];
sellersTableView.delegate = self;
sellersTableView.dataSource = self;
sellersTableView.backgroundColor = [[UIColor grayColor] colorWithAlphaComponent:0.05];
sellersTableView.separatorColor = [[UIColor grayColor] colorWithAlphaComponent:0.15];
sellersTableView.rowHeight = 44;
sellersTableView.layer.opacity = 0;
[self.companyView addSubview:sellersTableView];
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{sellersTableView.layer.opacity = 1;} completion:nil];
}
cellForRowAtIndexPath
if (tableView == sellersTableView) {
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.backgroundColor = [UIColor clearColor];
if ([sellersArray count] > 0) {
cell.textLabel.text = [sellersArray objectAtIndex:indexPath.row];
} else {
UILabel *noSellersYetLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, sellersTableView.frame.size.width, [self heightForTableView])];
noSellersYetLabel.text = #"no sellers yet";
noSellersYetLabel.textAlignment = NSTextAlignmentCenter;
noSellersYetLabel.textColor = [UIColor grayColor];
[cell addSubview:noSellersYetLabel];
sellersTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
}
removing
- (void) textFieldDidEndEditing:(UITextField *)textField
{
if (textField == addSellerTextField) {
[self updateSellers:textField];
}
}
- (void)updateSellers:(UITextField *)textField
{
[textField resignFirstResponder];
[self hideSellersTableView];
}
- (void)hideSellersTableView
{
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{sellersTableView.layer.opacity = 0;} completion:nil];
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
[sellersTableView removeFromSuperview];
sellersTableView = nil;
}
Solution
So apparently putting the dataSource = nil and delegate = nil into textFieldDidEndEditing fixed the problem. Thanks everybody for the answers!
It's strange behaviour of UITableView. The easiest way to resolve this issue just set the dataSource and delegate property of UITAbleView to nil before you make a call of function popToRootViewControllerAnimated. Furthermore you can use more common solution and add the code that set the properties to nil into the -dealloc method. In addition you no need the -killScroll method.
After a short research I have realized what the problem is. This unusual behaviour appeared in iOS 7. The scroll view retained by its superview may send message to delegate after the delegate is released. It happens due to -removeFromSuperview implementation UIScrollView triggers -setContentOffset: and, eventually, send message to delegate.
Just add following lines at the beginning of dealloc method:
sellersTableView.delegate = nil;
sellersTableView.dataSource = nil;
No need to use hacks like your killScroll method.
Also, I can't see why you want to call both popToRootViewController and dismissViewController.
If you dismiss a view controller which is embedded in a navigation controller, navigation controller itself as well as all contained view controllers will be released.
In your case you'll have just weird animation.
setContentOffset method won't help you, try to set
sellersTableView.dataSource = nil;
somewhere in your viewWillDisappear method.
This is not a good practice of course.
Change you closeViewController like below and see if works
(void) closeViewController
{
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
I don't think that setting the tableView (or it's delegate) to nil is the issue. You should be able to perform both dismissViewControllerAnimated or popToRootViewController individually without having to modify the tableView in this way.
So the issue is most likely due to calling both of these methods at the same time (and with animated = YES), and in doing so asking your viewController setup to do something unnatural.
Looks like upon tapping a "close" button you are both popping to a rootViewController of a UINavigationController, as well as dismissing a modal viewController.
In doing so, you're dismissing a modal viewController which is likely presented by the topViewController of the navigationController (so top vc is holding a reference to modal vc). AND you're trying to kill the top vc via the popToRootViewController method call. And you're doing both of these things using animated = YES, which means they take some time to complete, and you can't be sure when each finishes (ie you can't be sure when dealloc will be called).
Depending on your needs you could do one of several things.
Consider adding a delegate property to your modal vc. Dismiss the modal vc, and in the completionBlock of the modal vc tell its delegate that it's finished dismissing. At that point call popToRootViewController (because at this point you can be sure that the modal is gone and scrolling wasn't interrupted).
If it's your navController that's been presented modally, then do this in the opposite order. Notifying the delegate that the pop operation has completed, and do the modal dismissal then.
i want to start a new animation routine as soon as the previous one completes. When the previous does complete however, the new one does not get triggered since the delegate method is not being called. I have delegated a view controller to handle the animations of a button's CALayer. The 'buttonSlide' is the previous animation while 'buttonFade' is the new one.
Here's the code snippet:-
-(void)viewWillAppear:(BOOL)animated{
NSLog(#"TimeView appearing...");
[super viewWillAppear:animated];
[self pressButton:nil]; // Shows current time as soon as TimeView appears
// Silver challenge
CABasicAnimation *buttonSlide= [CABasicAnimation animationWithKeyPath:#"position"];
[buttonSlide setFromValue:[NSValue valueWithCGPoint:CGPointMake(320, 191+15)]];
[buttonSlide setToValue:[NSValue valueWithCGPoint:CGPointMake(160, 191+15)]];
[buttonSlide setDuration:.12];
[button.layer addAnimation:buttonSlide forKey:#"buttonSlide"];
[buttonSlide setDelegate:self];
CABasicAnimation *buttonFade= [CABasicAnimation animationWithKeyPath:#"opacity"];
[buttonFade setFromValue:[NSNumber numberWithFloat:.2]];
[buttonFade setToValue:[NSNumber numberWithFloat:1]];
[buttonFade setDuration:10];
}
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
NSLog(#"%# finshed: %d",anim,flag);
CAAnimation *buttonFade= [button.layer animationForKey:#"opacity"];
[button.layer addAnimation:buttonFade forKey:#"buttonFade"];
}
The delegate method isn't logging to the console as its not even getting called..Please assist.
Put this:
[buttonSlide setDelegate:self];
Before:
[button.layer addAnimation:buttonSlide forKey:#"buttonSlide"];
Thus becomes:
[buttonSlide setDelegate:self];
[button.layer addAnimation:buttonSlide forKey:#"buttonSlide"];
Would work.
Hope this helps.
I am using setNeedsDisplay on my GUI, but there update is sometimes not done. I am using UIPageControllView, each page has UIScrollView with UIView inside.
I have the following pipeline:
1) application comes from background - called applicationWillEnterForeground
2) start data download from server
2.1) after data download is finished, trigger selector
3) use dispatch_async with dispatch_get_main_queue() to fill labels, images etc. with new data
3.1) call setNeedsDisplay on view (also tried on scroll view and page controller)
Problem is, that step 3.1 is called, but changes apper only from time to time. If I swap pages, the refresh is done and I can see new data (so download works correctly). But without manual page turn, there is no update.
Any help ?
Edit: code from step 3 and 3.1 (removed _needRefresh variables pointed in comments)
-(void)FillData {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *stateID = [DataManager ConvertStateToStringFromID:_activeCity.actual_weather.state];
if ([_activeCity.actual_weather.is_night boolValue] == YES)
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#_noc", [_bgs objectForKey:stateID]]];
if (_isNight == NO)
{
_bgTransparencyInited = NO;
}
_isNight = YES;
}
else
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#", [_bgs objectForKey:stateID]]];
if (_isNight == YES)
{
_bgTransparencyInited = NO;
}
_isNight = NO;
}
[self.contentBgImage setNeedsDisplay]; //refresh background image
[self CreateBackgroundTransparency]; //create transparent background if colors changed - only from time to time
self.contentView.parentController = self;
[self.contentView FillData]; //Fill UIView with data - set labels texts to new ones
//_needRefresh is set to YES after application comes from background
[self.contentView setNeedsDisplay]; //This do nothing ?
[_grad display]; //refresh gradient
});
}
And here is selector called after data download (in MainViewController)
-(void)FinishDownload:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
[_activeViewController FillData]; //call method shown before
//try call some more refresh - also useless
[self.pageControl setNeedsDisplay];
//[self reloadInputViews];
[self.view setNeedsDisplay];
});
}
In AppDelegate I have this for application comes from background:
-(void)applicationWillEnterForeground:(UIApplication *)application
{
MainViewController *main = (MainViewController *)[(SWRevealViewController *)self.window.rootViewController frontViewController];
[main UpdateData];
}
In MainViewController
-(void)UpdateData
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(FinishForecastDownload:) name:#"FinishDownload" object:nil]; //create selector
[[DataManager SharedManager] DownloadForecastDataWithAfterSelector:#"FinishDownload"]; //trigger download
}
try this:
[self.view performSelectorOnMainThread:#selector(setNeedsLayout) withObject:nil waitUntilDone:NO];
or check this link:
http://blackpixel.com/blog/2013/11/performselectoronmainthread-vs-dispatch-async.html
setNeedsDisplay triggers drawRect: and is used to "redraw the pixels" of the view , not to configure the view or its subviews.
You could override drawRect: and modify your labels, etc. there but that's not what it is made for and neither setNeedsLayout/layoutSubviews is.
You should create your own updateUI method where you use your fresh data to update the UI and not rely on specialized system calls meant for redrawing pixels (setNeedsDisplay) or adjusting subviews' frames (drawRect:).
You should set all your label.text's, imageView.image's, etc in the updateUI method. Also it is a good idea to try to only set those values through this method and not directly from any method.
None of proposed solutions worked. So at the end, I have simply remove currently showed screen from UIPageControllView and add this screen again. Something like changing the page there and back again programatically.
Its a bit slower, but works fine.
I have a custom CALayer with an animatable property called progress. The basic code is:
#implementation AnimatedPieChartLayer
#dynamic progress;
+ (BOOL)needsDisplayForKey:(NSString *)key {
return [key isEqualToString: #"progress"] || [super needsDisplayForKey: key];
}
- (id <CAAction>)actionForKey:(NSString *)key {
if ([self presentationLayer] != nil) {
if ([key isEqualToString: #"progress"]) {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath: key];
[anim setFromValue: [[self presentationLayer] valueForKey: key]];
[anim setDuration: 0.75f];
return anim;
}
}
return [super actionForKey: key];
}
- (void)drawInContext:(CGContextRef)context {
// do stuff
}
#end
In the view controller I do this, to try and get my animation to start from the beginning every time:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear: animated];
[pieChart setProgress: 0.0];
}
This all works perfectly. I put the layer on a view, the view in a view controller, and it all works as expected.
The complication is that my view controller is inside a UIScrollView. This presents a situation where the user could swipe away from my view controller before the CALayer animation completes. If they swipe back quickly, the CALayer animation does something weird. It reverses. It's like the animation is trying to go back to the beginning. Once it gets there it starts over, and animates the way it is supposed to.
So the question is, how can I prevent this. I want the animation to start from the beginning every time the view is displayed – regardless of what happened last time.
Any suggestions?
UPDATE
Here's a visual example of the problem:
Working:
Failing:
In the setup you have there, you have an implicit animation for the key "progress" so that whenever the progress property of the layer changes, it animates. This works both when the value increases and when it decreases (as seen in your second image).
To restore the layer to the default 0 progress state without an animation, you can wrap the property change in a CATransaction where all actions are disabled. This will disable the implicit animation so that you can start over from 0 progress.
[CATransaction begin];
[CATransaction setDisableActions:YES]; // all animations are disabled ...
[pieChart setProgress: 0.0];
[CATransaction commit]; // ... until this line
Probably too simple a suggestion to merit an 'answer,' but I would suggest canceling the animation. Looks like you could rewrite this code pretty easily so that the progress updates were checking some complete flag and then when the view get hidden, kill it. This thread has some good ideas on it.