17

I'm creating an autolayout-friendly split view class for one of my applications. Among its various features is that it can collapse panes, and can animate their collapse, much as you might have seen NSSplitView do.

Since I'm using constraints, I'm achieving this by placing a required width = (current width) constraint on the pane, and then setting the constraint's constant to 0 in an animated fashion:

- (NSLayoutConstraint*)newHiddenConstraintAnimated:(BOOL)animated {
    NSLayoutConstraint * constraint = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:NSWidth(self.view.frame)];
    constraint.priority = NSLayoutPriorityRequired;

    CABasicAnimation * anim = [CABasicAnimation animation];
    anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    anim.duration = 0.2;
    constraint.animations = [NSDictionary dictionaryWithObject:anim forKey:@"constant"];

    [self.view addConstraint:constraint];

    [(animated ? constraint.animator : constraint) setConstant:0.0];

    return constraint;
}

This works beautifully. Unfortunately, expanding the pane later does not fare so well.

- (void)removeHiddenConstraintAnimated:(BOOL)animated {
    if(!animated) {
        [self.view removeConstraint:self.hiddenConstraint];
    }
    else {
        NSLayoutConstraint * constraint = self.hiddenConstraint;
        NSView * theView = self.view;

        [NSAnimationContext beginGrouping];

        [constraint.animator setConstant:self.width];

        [NSAnimationContext currentContext].completionHandler = ^{
            [theView removeConstraint:constraint];
        };

        [NSAnimationContext endGrouping];
    }

    self.hiddenConstraint = nil;
}

If I insert some timing code, I can see that the completion handler fires almost instantly, removing the constraint before it has time to animate. Setting a duration on the NSAnimationContext has no effect.

Any idea what I could be doing wrong here?

Becca Royal-Gordon
  • 17,541
  • 7
  • 56
  • 91
  • Did you ever finish this split view class? Possibility of it going open-source? – sudo rm -rf Jul 21 '12 at 22:11
  • I don't plan to at the moment. It's fairly specialized for this application, and I believe `NSSplitView` in [PURRDACTED] has been redesigned to work better with autolayout. – Becca Royal-Gordon Jul 22 '12 at 02:19
  • Oh, gotcha. In regards to [redacted], it does have that nice feature in regards to auto layout, but of course it's not backwards-compatible with older targets. Oh well, I'll make my own I guess! :) – sudo rm -rf Jul 22 '12 at 03:04

4 Answers4

17

You have to first set the completion handler and only then send the message to the animator proxy. Otherwise, it seems that setting the completion handler after the animation started fires it immediately and the constant is removed before the animation has time to finish. I have just checked this with a piece of simple code:

[NSAnimationContext beginGrouping];
NSAnimationContext.currentContext.duration = animagionDuration;
NSAnimationContext.currentContext.completionHandler = ^{
  [self removeConstraint:collapseConstraint];
};
[collapseConstraint.animator setConstant:expandedHeight];

[NSAnimationContext endGrouping]; This works perfectly, but if you set completion handler after -setConstant:, the animation does not have a chance to run.

Sangram Shivankar
  • 3,535
  • 3
  • 26
  • 38
skh
  • 374
  • 1
  • 7
  • Not at all, some time ago I hit the same issue, and it was your question that led me to experiment :) – skh Apr 02 '12 at 02:06
13

I agree, this is pretty strange, and could well be a bug. I'd definitely report it as such because, to the best of my knowledge, this should work.

I was able to get it to work by using the NSAnimationContext class method +runAnimationGroup:completionHandler: instead of the beginGrouping and endGrouping statements:

[NSAnimationContext runAnimationGroup:^(NSAnimationContext* context){
    [constraint.animator setConstant:self.width];   
} completionHandler:^(void){
    [theView removeConstraint:constraint];
    NSLog(@"completed");
}];
Rob Keniger
  • 45,830
  • 6
  • 101
  • 134
3

The completion handler is firing immediately because it thinks there aren't any animations that need to be run. I would check and confirm that the animation you created is still attached to the view. By default CABasicAnimation is set to remove itself upon completion by way of the removedOnCompletion property it inherits from CAAnimation (which by default is set to YES).

you'll want to

anim.removedOnCompletion = NO;
Bhavin Bhadani
  • 22,224
  • 10
  • 78
  • 108
rudy
  • 1,702
  • 11
  • 14
1

I'm just getting to grips with this stuff myself so this may be a naive analysis but:

It seems to me that you are specifying that an animation on the constraints' properties (in your else block) but, then, immediately setting the reference to the constraint to nil (potentially releasing it) before the animation has a chance to run.

I would expect that you would want to set hiddenConstraint to nil from within, or triggered by, the animation completion block.

Note that if, as is likely, I am wrong I would appreciate a word or two about why to help me understand it better :)

Matt Mower
  • 3,743
  • 1
  • 18
  • 12
  • It's reasonable to assume that a view owns any constraints you add to it, so even if the constraint is not referenced through the `hiddenConstraint` property, it should still be referenced by the view's list of constraints. It's also kept alive by the block itself, since blocks retain the values of any object variables you use in them. – Peter Hosey Mar 14 '12 at 18:21
  • As Peter said, `constraint` is a strong reference and is captured by the block, and even if it wasn't the view retains the constraint. There are hundreds of constraints in a moderately complex autolayout, and the vast majority are only referenced by the views that own them. – Becca Royal-Gordon Mar 16 '12 at 08:47