3

I'm attempting to implement an animation that shows/hides a view in a horizontal arrangement. I'd like this to happen with slide, and with no opacity changes. I'm using auto-layout everywhere.

Critically, the total width of the containing view changes with the window. So, constant-based animations are not possible (or so I believe, but happy to be proved wrong).

|- viewA -|- viewB -|

My first attempt was to use NSStackView, and animate the isHidden property of an arranged subview. Despite seeming like it might do the trick, I was not able to pull off anything close to what I was after.

My second attempt was to apply two constraints, one to force viewB to be zero width, and a second to ensure the widths are equal. On animation I change the priorities of these constraints from defaultHigh <-> defaultLow.

This results in the correct layout in both cases, but the animation is not working out.

With wantsLayer = true on the containing view, no animation occurs whatsoever. The views just jump to their final states. Without wantsLayer, the views do animate. However, when collapsing, viewA does a nice slide, but viewB instantly disappears. As an experiment, I changed the zero width to a fixed 10.0, and with that, the animation works right in both directions. However, I want the view totally hidden.

So, a few questions:

Is it possible to animate layouts like this with layer-backed views?

Are there other techniques possible for achieving the same effect?

Any ideas on how to achieve these nicely with NSStackView?

class LayoutAnimationViewController: NSViewController {
    let containerView: NSView
    let view1: ColorView
    let view2: ColorView
    let widthEqualContraint: NSLayoutConstraint
    let widthZeroConstraint: NSLayoutConstraint

    init() {
        self.containerView = NSView()
        self.view1 = ColorView(color: NSColor.red)
        self.view2 = ColorView(color: NSColor.blue)

        self.widthEqualContraint = view2.widthAnchor.constraint(equalTo: view1.widthAnchor)
        widthEqualContraint.priority = .defaultLow

        self.widthZeroConstraint = view2.widthAnchor.constraint(equalToConstant: 0.0)
        widthZeroConstraint.priority = .defaultHigh

        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func loadView() {
        self.view = containerView
//        view.wantsLayer = true

        view.addSubview(view1)
        view.addSubview(view2)
        view.subviewsUseAutoLayout = true

        NSLayoutConstraint.activate([
            view1.topAnchor.constraint(equalTo: view.topAnchor),
            view1.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            view1.leadingAnchor.constraint(equalTo: view.leadingAnchor),
//            view1.trailingAnchor.constraint(equalTo: view2.leadingAnchor),

            view2.topAnchor.constraint(equalTo: view.topAnchor),
            view2.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            view2.leadingAnchor.constraint(equalTo: view1.trailingAnchor),
            view2.trailingAnchor.constraint(equalTo: view.trailingAnchor),

            widthEqualContraint,
            widthZeroConstraint,
        ])
    }

    func runAnimation() {
        view.layoutSubtreeIfNeeded()

        self.widthEqualContraint.toggleDefaultPriority()
        self.widthZeroConstraint.toggleDefaultPriority()
//        self.leadingConstraint.toggleDefaultPriority()

        NSAnimationContext.runAnimationGroup({ (context) in
            context.allowsImplicitAnimation = true
            context.duration = 3.0

            self.view.layoutSubtreeIfNeeded()
        }) {
            Swift.print("animation complete")
        }
    }
}

extension LayoutAnimationViewController {
    @IBAction func runTest1(_ sender: Any?) {
        self.runAnimation()
    }
}

Also, some potentially relevant, but so far unhelpful, related questions:

Animating Auto Layout changes concurrently with NSPopover contentSize change

Animating Auto Layout constraints with NSView.layoutSubtreeIfNeeded() not working on macOS High Sierra

Hide view item of NSStackView with animation

Mattie
  • 2,868
  • 2
  • 25
  • 40

0 Answers0