Search code examples
objective-ciosuiviewuibuttonuiviewanimationtransition

How to commit a UIButton state change before starting other UIView animations?


I have a UIButton which fires a target action causing one view to transition to another on touchupinside.

When this happens the I also change the button's state to selected but the (different) background image for the selected state doesn't actually draw until the UIView transition is complete (or sometimes it also seems to transition with the UIView transition.)

How can I force the button to redraw the new background image before starting the view transition?

The code that runs on UIControlEventTouchUpInside:

RhSubTab *activeTab;
for(RhSubTab *tab in self.tabViews.copy) {
    tab.selected = NO;
    [self.view bringSubviewToFront:tab];
    if(tab.tag == type) activeTab = tab;
}
[self.view bringSubviewToFront:activeTab];
[activeTab setSelected:YES];

UIView *oldView;
UIView *newView;
for(UIView *view in self.contentViews.copy) {
    if([self.view.subviews containsObject:view]) {
        oldView = view;
    }
    if(view.tag == type) {
        newView = view;
    }
}

if(!oldView) [self.view addSubview:newView];
else [UIView transitionFromView:oldView toView:newView duration:0.5 options:(UIViewAnimationOptionTransitionCrossDissolve|UIViewAnimationCurveEaseInOut) completion:nil];

And the custom (RhSubTab) button class:

- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if(self) {
    [self setBackgroundImage:[[UIImage imageNamed:@"SubContentTabBg.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 9, 0, 40)] forState:UIControlStateNormal];
    [self setBackgroundImage:[[UIImage imageNamed:@"SubContentTabBg-selected.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(0, 9, 0, 40)] forState:UIControlStateSelected];
}
return self;
}

Solution

  • Try breaking out the second part of your method (after setting the button to the selected state) into a second method.

    Then use performSelector:withObject:afterDelay: to call that second method

    So your method might look like this:

    RhSubTab *activeTab;
    for(RhSubTab *tab in self.tabViews.copy) {
        tab.selected = NO;
        [self.view bringSubviewToFront:tab];
        if(tab.tag == type) activeTab = tab;
    }
    [self.view bringSubviewToFront:activeTab];
    [activeTab setSelected:YES];
    
    [self performSelector: @selector(doTransition) withObject: nil afterDelay: 0];
    

    And your second method might look like this:

    - (void) doTransition;
    {
      UIView *oldView;
      UIView *newView;
      for(UIView *view in self.contentViews.copy) {
        if([self.view.subviews containsObject:view]) {
          oldView = view;
        }
        if(view.tag == type) {
          newView = view;
        }
      }
    
      if(!oldView) [self.view addSubview:newView];
      else [UIView transitionFromView: oldView 
        toView:newView duration:0.5 
        options: (UIViewAnimationOptionTransitionCrossDissolve|UIViewAnimationCurveEaseInOut)
        completion:nil];
    }
    

    Even with a delay value of 0, the performSelector:withObject:afterDelay: returns immediately, and doesn't trigger the specified method until the next path through the event loop, after checking for user events, processing screen updates, etc.

    If you want the transition to wait until the button change is complete, you might want to add a small non-zero delay value.

    The method performSelector:withObject:afterDelay: will let you pass 0 or 1 objects as parameters. At a glance, it did not look like the second half of your transition method needed any parameters.