Search code examples
objective-cconstraintscore-animationnsview

Animate NSView constraints (layer backed view) on macOS objective-c


I am on XCode 12, macOS, objective-c, not iOS.

I have a menu (horizontal stack view) with 3 items:

enter image description here

The blue underline is created programmatically:

- (void)viewDidLoad {

    [super viewDidLoad];     
    [self createAnimatedLineAtButton:self.importButton];
}

// Create line
- (void)createAnimatedLineAtButton:(NSButton*)button {

    // This view is layer backed
    SHViewCustomBackground* underlineView = [[SHViewCustomBackground alloc] init];
    
    underlineView.backgroundColor = [NSColor colorNamed:@"color_spot"]; // blue color
    underlineView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:underlineView];
    self.underlineView = underlineView;    

    [self updateAnimatedLineUnderButton:self.importButton];
}

The next codes updates the constraints (initially called on line creation above) and later on the click of each button.

- (void)updateAnimatedLineUnderButton:(NSButton *)button {
    
    // Constraints are stored here
    if (self.underlineConstraints) {
        [self.view removeConstraints:self.underlineConstraints];
    }

    [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){
        context.duration = 1;
        context.allowsImplicitAnimation = YES;
    
        NSView* underlineView = self.underlineView;
        NSDictionary *views = NSDictionaryOfVariableBindings(underlineView, button);
        self.underlineConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[button]-[underlineView(1)]" options:NSLayoutFormatAlignAllLeading | NSLayoutFormatAlignAllTrailing metrics:nil views:views];
        [self.view addConstraints:self.underlineConstraints];
    
        [self.view layoutSubtreeIfNeeded];
    } completionHandler:nil];    
}

Here are the button actions

- (IBAction)buttonClicked:(id)sender {
    
    [self updateAnimatedLineUnderButton:(NSButton*)sender];
}

Now my question: When i click buttons in this order "Import" > "Export" > "Import" the line animates as expected from Import to Export and back. Whenever i click on "Placeholder" (middle button) the line just jumps and doesn't animate. When i click on "Import" or "Export" afterwards it jumps too.

enter image description here

Im banging my head on whats wrong here. All buttons are connected correctly, all actions are triggered correctly.

EDIT: After comparing all buttons i discovered that both import and export button (where it works) have the same width.


Solution

  • Here is an idea ... it works. Initially I was going to do it your way but, you may not like this, ended up animated two constraints. But it works nicely.

    enter image description here

    The idea is very similar to yours but the difference here is that I create a leading and a trailing constraint and animate that in stead.

    @implementation ViewController
    
    - ( void ) viewDidLoad
    {
        super.viewDidLoad;
        [self animateLine:self.longButton];
    }
    
    // Animate the line
    - ( void ) animateLine:( NSButton * ) button
    {
        [NSAnimationContext runAnimationGroup: ^ ( NSAnimationContext * context ) {
    
            context.duration = 1;
            context.allowsImplicitAnimation = YES;
            self.leadingConstraint.animator.constant = button.frame.origin.x;
            self.trailingConstraint.animator.constant = self.stackView.bounds.size.width - button.frame.origin.x - button.frame.size.width;
            NSLog ( @"Setting leading to %f trailing to %f", self.leadingConstraint.constant, self.trailingConstraint.constant );
    
            self.lineField.layoutSubtreeIfNeeded;
    
        }];
    }
    
    - ( IBAction ) buttonAction:( id ) sender
    {
        [self animateLine:sender];
    }
    
    

    This is it. The rest happens in storyboard. I set the constraints up so that the distances make sense by swapping the first and second item to taste.