Soft scroll animation NSScrollView scrollToPoint:

scrollToPoint is not animatable. Only animatable properties like bounds and position in NSAnimatablePropertyContainer are animated. You don't need to do anything with CALayer: remove the wantsLayer and CALayer stuff. Then with following code it is animated.

- (void)scrollToXPosition:(float)xCoord {
    [NSAnimationContext beginGrouping];
    [[NSAnimationContext currentContext] setDuration:5.0];
    NSClipView* clipView = [_scrollView contentView];
    NSPoint newOrigin = [clipView bounds].origin;
    newOrigin.x = xCoord;
    [[clipView animator] setBoundsOrigin:newOrigin];
    [_scrollView reflectScrolledClipView: [_scrollView contentView]]; // may not bee necessary
    [NSAnimationContext endGrouping];
}

The proposed answers have a significant downside: If the user tries to scroll during an ongoing animation, the input will be cause jittering as the animation will forcefully keep on going until completion. If you set a really long animation duration, the issue becomes apparent. Here is my use case, animating a scroll view to snap to a section title (while trying to scroll up at the same time):

enter image description here

I propose the following subclass:

public class AnimatingScrollView: NSScrollView {

    // This will override and cancel any running scroll animations
    override public func scroll(_ clipView: NSClipView, to point: NSPoint) {
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        contentView.setBoundsOrigin(point)
        CATransaction.commit()
        super.scroll(clipView, to: point)
    }

    public func scroll(toPoint: NSPoint, animationDuration: Double) {
        NSAnimationContext.beginGrouping()
        NSAnimationContext.current.duration = animationDuration
        contentView.animator().setBoundsOrigin(toPoint)
        reflectScrolledClipView(contentView)
        NSAnimationContext.endGrouping()
    }

}

By overriding the normal scroll(_ clipView: NSClipView, to point: NSPoint) (invoked when the user scrolls) and manually performing the a scroll inside a CATransaction with setDisableActions, we cancel the current animation. However, we don't call reflectScrolledClipView, instead we call super.scroll(clipView, to: point), which will perform other necessary internal procedures and then perform reflectScrolledClipView.

Above class produces better results:

enter image description here


Swift 4 code of this answer

func scroll(toPoint: NSPoint, animationDuration: Double) {
    NSAnimationContext.beginGrouping()
    NSAnimationContext.current.duration = animationDuration
    let clipView = scrollView.contentView
    clipView.animator().setBoundsOrigin(toPoint)
    scrollView.reflectScrolledClipView(scrollView.contentView)
    NSAnimationContext.endGrouping()
}