Hide view item of NSStackView with animation

3.3k views Asked by At

I working with swift 4 for macOS and I would like to hide an stack view item with animation.

I tried this:

class ViewController: NSViewController {

    @IBOutlet weak var box: NSBox!
    @IBOutlet weak var stack: NSStackView!
    var x = 0



    @IBAction func action(_ sender: Any) {
        if x == 0 {

            NSAnimationContext.runAnimationGroup({context in
                context.duration = 0.25
                context.allowsImplicitAnimation = true

                self.stack.arrangedSubviews.last!.isHidden = true
                self.view.layoutSubtreeIfNeeded()
                x = 1
            }, completionHandler: nil)

        } else {

            NSAnimationContext.runAnimationGroup({context in
                context.duration = 0.25
                context.allowsImplicitAnimation = true

                self.stack.arrangedSubviews.last!.isHidden = false
                self.view.layoutSubtreeIfNeeded()
                x = 0
            }, completionHandler: nil)

        }

    }
}

The result will be:

enter image description here

It works! But I am not happy with the animation style. My wish is:

  • I press the button, the red view will be smaller to the right side
  • I press the button, the red view will be larger to the left side.

Like a sidebar or if you have an splitview controller and you will do splitviewItem.animator().isCollapsed = true

this animation of show/hide is perfect. Is this wish possible?

UPDATE self.stack.arrangedSubviews.last!.animator().frame = NSZeroRect

enter image description here

UPDATE 2

self.stack.arrangedSubviews.last!.animator().frame = NSRect(x: self.stack.arrangedSubviews.last!.frame.origin.x, y: self.stack.arrangedSubviews.last!.frame.origin.y, width: 0, height: self.stack.arrangedSubviews.last!.frame.size.height)

enter image description here

2

There are 2 answers

5
brianLikeApple On BEST ANSWER

I just create a simple testing code which can animate the red view, instead of using button, I just used touchup, please have a look at the code:

class ViewController: NSViewController {

let view1 = NSView()
let view2 = NSView()
let view3 = NSView()
var x = 0

var constraint: NSLayoutConstraint!

override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
    view1.wantsLayer = true
    view2.wantsLayer = true
    view3.wantsLayer = true

    view1.layer?.backgroundColor = NSColor.orange.cgColor
    view2.layer?.backgroundColor = NSColor.green.cgColor
    view3.layer?.backgroundColor = NSColor.red.cgColor

    view1.translatesAutoresizingMaskIntoConstraints = false
    view2.translatesAutoresizingMaskIntoConstraints = false
    view3.translatesAutoresizingMaskIntoConstraints = false

    self.view.addSubview(view1)
    self.view.addSubview(view2)
    self.view.addSubview(view3)

    self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view1]|", options: [], metrics: nil, views: ["view1": view1]))
    self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view2]|", options: [], metrics: nil, views: ["view2": view2]))
    self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view3]|", options: [], metrics: nil, views: ["view3": view3]))
    self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view1(==view2)][view2(==view1)][view3]|", options: [], metrics: nil, views: ["view1": view1, "view2": view2, "view3": view3]))

    constraint = NSLayoutConstraint(item: view3, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: 100)
    self.view.addConstraint(constraint)
}

override var representedObject: Any? {
    didSet {
    // Update the view, if already loaded.
    }
}

override func mouseUp(with event: NSEvent) {
    if x == 0 {

        NSAnimationContext.runAnimationGroup({context in
            context.duration = 0.25
            context.allowsImplicitAnimation = true

            constraint.constant = 0
            self.view.layoutSubtreeIfNeeded()
            x = 1
        }, completionHandler: nil)

    } else {

        NSAnimationContext.runAnimationGroup({context in
            context.duration = 0.25
            context.allowsImplicitAnimation = true

            constraint.constant = 100
            self.view.layoutSubtreeIfNeeded()
            x = 0
        }, completionHandler: nil)

    }
}
}

enter image description here

0
catlan On

Have a look at Organize Your User Interface with a Stack View sample code.

I disliked the heightContraint on the view controller and the height calculation in the -viewDidLoad. The code below calculation the size before animating and only adds the contrain while animating.

Breakable Contraint

First you need to set the priority of the contraint to something lower than 1000 (required constraint) for the direction you want the animation. Here the bottom contraint.

While we animate the view in and out, we will add contraint to do so with a priority of 1000 (required constraint).

Code

@interface NSStackView (LOAnimation)
- (void)lo_toggleArrangedSubview:(NSView *)view;
@end

@implementation NSStackView (LOAnimation)
- (void)lo_toggleArrangedSubview:(NSView *)view
{
    NSAssert([self detachesHiddenViews], @"toggleArrangedSubview requires detachesHiddenViews YES");
    NSAssert([[self arrangedSubviews] containsObject:view], @"view not an arrangedSubview");
    CGFloat postAnimationHeight = 0;
    NSLayoutConstraint *animationContraint = nil;
    if (view.hidden) {
        view.hidden = NO; // NSStackView will re-add view to its subviews
        [self layoutSubtreeIfNeeded]; // calucalte view size
        postAnimationHeight = view.bounds.size.height;
        animationContraint = [view.heightAnchor constraintEqualToConstant:0];
        animationContraint.active = YES;
    } else {
        [self layoutSubtreeIfNeeded];  // calucalte view size
        animationContraint = [view.heightAnchor constraintEqualToConstant:view.bounds.size.height];
        animationContraint.active = YES;
    }
    [self layoutSubtreeIfNeeded]; // layout with animationContraint in place
    [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
        context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
        animationContraint.animator.constant = postAnimationHeight;
        [self layoutSubtreeIfNeeded];
    } completionHandler:^{
        view.animator.hidden = (postAnimationHeight == 0);
        [view removeConstraint:animationContraint];
    }];
}
@end