swiftmacoscocoaanimationnsanimationcontext

NSAnimationContext does not animate frame size for NSButton


Using NSAnimationContext.runAnimationGroup(_:_:) as demonstrated in the NSAnimationContext documentation to animate both the frame origin and size works as expected for some view types (including NSImageView). However, it does not work as expected for an NSButton unless I add an explicit frame size change after the animation

Animating frame size for NSImageView

The following works as expected for an NSImageView. It is moved to the origin, and resized to 200x200:

NSAnimationContext.runAnimationGroup({(let context) -> Void in
    context.duration = 2.0
    // Get the animator for an NSImageView
    let a = self.theImage.animator()
    // Move and resize the NSImageView
    a.frame = NSRect(x: 0, y: 0, width: 200, height: 200)
}) {
    print("Animation done")
}

Animating frame size for NSButton

When performing the same with an NSButton, the button will move but not resize:

NSAnimationContext.runAnimationGroup({(let context) -> Void in
    context.duration = 2.0
    // Get the animator for an NSButton
    let a = self.button.animator()
    // Move and resize the NSImageView
    a.frame = NSRect(x: 0, y: 0, width: 200, height: 200)
}) {
    print("Animation done")
}

However, if I add the following line of code to the end, after all of the animation code, it works as expected!

self.button.frame = NSRect(x: 0, y: 0, width: 200, height: 200)

The final, working listing for NSButton is:

NSAnimationContext.runAnimationGroup({(let context) -> Void in
    context.duration = 2.0
    // Get the animator for an NSButton
    let a = self.button.animator()
    // Move and resize the NSImageView
    a.frame = NSRect(x: 0, y: 0, width: 200, height: 200)
}) {
    print("Animation done")
}
self.button.frame = NSRect(x: 0, y: 0, width: 200, height: 200)

I'm not looking a gift horse in the mouth here, but I don't understand why this is required for NSButton, or even what makes it work. Can anyone explain why explicitly setting the frame of the NSButton after the animation code makes the animation work?


Solution

  • I suspect this has something to do with implicit autolayout constraints being generated at runtime. After modifying the frame, the autolayout simply reverts it back to the original size.

    I abandoned the original approach in favor of the following:

    1. Create width and/or height constraints in Interface Builder. I use the default priority of 1000 (constraint is mandatory).
    2. Create an outlet for the width and/or height NSLayoutConstraint. This was tricky in IB: I had to double-click the constraint in the measurements tab of the inspector which then opened the constraint object in the inspector. Then you can select the connections tab and connect the outlet.
    3. In my NSViewController subclass I use anchors to define the new height or width: theWidthConstraint.constant = 200 followed by self.view.needsUpdateConstraints = true

    This approach is much cleaner and more compatible with the autolayout system. In addition, it makes it easy to animate the entire layout change that results from the new autolayout:

    NSAnimationContext.runAnimationGroup({(let context) -> Void in
        context.duration = 1.0
        self.theWidthConstraint.animator().constant = 200
        // Other constraint modifications can go here
    }) {
        print("Animation done")
    }