3

I'm trying to write a custom UIButton subclass that will "animate" during press and release.

When pressed, the button should "shrink" (toward its center) to 90% of its original size. When released, the button should "expand" to 105%, shrink again to 95% and then return to its original size.

Here's the code I've got right now:

#pragma mark -
#pragma mark - Touch Handling
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    [self animatePressedDown];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    [self animateReleased];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    [self animateReleased];
}

- (void)animatePressedDown {
    NSLog(@"button.frame before animatedPressedDown: %@", NSStringFromCGRect(self.frame));
    [self addShadowLayer];
    CATransform3D ninetyPercent = CATransform3DMakeScale(0.90f, 0.90f, 1.00f);
    [UIView animateWithDuration:0.2f
                     animations:^{
                         self.layer.transform = ninetyPercent;
                     }
                     completion:^(BOOL finished) {
                         NSLog(@"button.frame after animatedPressedDown: %@", NSStringFromCGRect(self.frame));
                     }
     ];
}
- (void)animateReleased {
    [self.shadowLayer removeFromSuperlayer];
    CATransform3D oneHundredFivePercent = CATransform3DMakeScale(1.05f, 1.05f, 1.00f);
    CATransform3D ninetyFivePercent     = CATransform3DMakeScale(0.95f, 0.95f, 1.00f);
    [UIView animateWithDuration:0.1f
                     animations:^{
                         self.layer.transform = oneHundredFivePercent;
                     }
                     completion:^(BOOL finished) {
                         NSLog(@"button.frame after animateReleased (Stage 1): %@", NSStringFromCGRect(self.frame));
                         [UIView animateWithDuration:0.1f
                                          animations:^{
                                              self.layer.transform = ninetyFivePercent;
                                          }
                                          completion:^(BOOL finished) {
                                              NSLog(@"button.frame after animateReleased (Stage 2): %@", NSStringFromCGRect(self.frame));
                                              [UIView animateWithDuration:0.1f
                                                               animations:^{
                                                                   self.layer.transform = CATransform3DIdentity;
                                                                   self.layer.frame     = self.frame;
                                                               }
                                                               completion:^(BOOL finished) {
                                                                   NSLog(@"button.frame after animateReleased (Stage 3): %@", NSStringFromCGRect(self.frame));
                                                               }
                                               ];
                                          }
                          ];
                     }
     ];
}

Anyway, the above code works perfectly... some of the time. At other times, the button animation works as expected, but after the "released" animation, the final frame of the button is "shifted" up and left from its original position. That's why I have those NSLog statements, to track exactly where the button's frame is during each stage of the animation. When the "shift" occurs, it happens somewhere between animatePressedDown and animateReleased. At least, the frame shown in animatePressedDown is ALWAYS what I expect it to be, but the first value for the frame in animateReleased is wrong frequently.

I can't see a pattern to the madness, though the same buttons in my app tend to behave correctly or incorrectly consistently from app run to app run.

I'm using auto layout for all of my buttons, so I can't figure out what the difference is to make one button behave correctly and another one change its position.

mbm29414
  • 11,558
  • 6
  • 56
  • 87

2 Answers2

3

I really hate when I answer my own questions, but I figured it out.

(I'd been struggling with this issue for days, but it finally clicked after I asked the question. Isn't that how it always works?)

Apparently, the problem for me was the fact that (on the buttons that were "shifting") I was changing the button's title mid-animation.

Once I set up a blocks-based system to call the title change only after the button's "bounce"/"pop" animation was complete, the problems went away.

I still don't know all of why this works the way it does, as the titles I was setting in no way changed the overall size of the buttons, but setting the buttons up as described fixed my problem.

Here's the code from my custom UIButton subclass, with the block property added:

@interface MSSButton : UIButton {

}
@property (copy  , nonatomic) void(^pressedCompletion)(void);
// Other, non-related stuff...
@end

@implementation MSSButton
#pragma mark -
#pragma mark - Touch Handling
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    [self animatePressedDown];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    [self animateReleased];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    [self animateReleased];
}

- (void)animatePressedDown {
    [self addShadowLayer];
    CATransform3D ninetyPercent = CATransform3DMakeScale(0.90f, 0.90f, 1.00f);
    [UIView animateWithDuration:0.2f
                     animations:^{
                         self.layer.transform = ninetyPercent;
                     }
     ];
}

- (void)animateReleased {
    [self.shadowLayer removeFromSuperlayer];
    CATransform3D oneHundredFivePercent = CATransform3DMakeScale(1.05f, 1.05f, 1.00f);
    CATransform3D ninetyFivePercent     = CATransform3DMakeScale(0.95f, 0.95f, 1.00f);
    [UIView animateWithDuration:0.1f
                     animations:^{
                         self.layer.transform = oneHundredFivePercent;
                     }
                     completion:^(BOOL finished) {
                         [UIView animateWithDuration:0.1f
                                          animations:^{
                                              self.layer.transform = ninetyFivePercent;
                                          }
                                          completion:^(BOOL finished) {
                                              [UIView animateWithDuration:0.1f
                                                               animations:^{
                                                                   self.layer.transform = CATransform3DIdentity;
                                                               }
                                                               completion:^(BOOL finished) {
                                                                   if (self.pressedCompletion != nil) {
                                                                       self.pressedCompletion();
                                                                       self.pressedCompletion = nil;
                                                                   }
                                                               }
                                               ];
                                          }
                          ];
                     }
     ];
}
@end

Since my IBAction fires before animateReleased (due to order of methods listed in my touch handling methods), I simply set the pressedCompletion block in my IBAction and the title is changed at the end of the animation sequence.

mbm29414
  • 11,558
  • 6
  • 56
  • 87
0

One likely problem is that you're trying to use the view's frame property after you've changed the transform property. The UIView class reference page specifically warns against that:

Warning: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.

Specifically, you're changing the layer's transform, which probably impacts the view's transform, and then accessing self.frame. You might want to ensure that the view's transform is set to CGAffineTransformIdentity before accessing self.frame.

Caleb
  • 124,013
  • 19
  • 183
  • 272
  • I understand why the view's frame should be **ignored**, as the documentation says, but you seem to be saying that my **accessing** the frame (I'm assuming for my `NSLog` statements) is somehow responsible. I went ahead and deleted the only line where I assign to the view's frame (in the last animation), and the results are unchanged. I added that line in as a way to see if I could keep the view from "shifting", and it appears that line does nothing whether included or not. So, I'm not sure the frame issue has anything to do with my issue. – mbm29414 Aug 15 '14 at 18:05