NSScrollview - Pagination effect - Stop animation

Hello, I am new to macOS development and Swift in general and am trying to animate a NSScrollview to create a pagination like effect.

NSAnimationContext.runAnimationGroup({ context in
            context.duration = 2
        
            scrollView.contentView.animator().setBoundsOrigin(NSMakePoint(0, offset))
        }) {
            
        }}

If the user now scrolls while the animation is running, the scrollview jumps around. I want to stop the animation at the current position so there is a callback to NSScrollView.willStartLiveScrollNotification

    @objc func scrollstarted() {
       print("Scroll Started")
            print(NSApp.currentEvent)
        var val = scrollView.contentView.frame.height
        var val2 = scrollView.documentView?.frame.height ?? 0;
        if(scrollView.contentView.bounds.origin.y>0 && scrollView.contentView.bounds.origin.y<=(val2-val)-1 ){
            //scrollView.contentView.animator().setBoundsOrigin(NSMakePoint(0, scrollView.contentView.bounds.origin.y))
            scrollView.contentView.layer?.removeAllAnimations()
        }

I tried now to either set a new destination bound or to remove all animations. But both do not change the "jumping" behavior and the animation still continues to its end. How do I stop a running animation?

Could you show more code, notably where you call scrollstarted()

You may use NSView.boundsDidChangeNotification, as described in my older post here below, to be notified of changes, and removeAnimations in the notification handler )

https://developer.apple.com/forums/thread/114195

Hello, this is all the corresponding code:

//

//  ViewController.swift

//  Test3

//

//  Created by Daniel Zollitsch on 27.09.21.

//



import Cocoa



class ViewController: NSViewController {

    let scrollView = NSScrollView(frame: NSRect(x: 300, y: 300, width: 600, height: 200))

    @objc func startscroll() {

       print("Scroll Started")

            print(NSApp.currentEvent)

        var val = scrollView.contentView.frame.height

        var val2 = scrollView.documentView?.frame.height ?? 0;

        if(scrollView.contentView.bounds.origin.y>0 && scrollView.contentView.bounds.origin.y<=(val2-val)-1 ){

            //scrollView.contentView.animator().setBoundsOrigin(NSMakePoint(0, scrollView.contentView.bounds.origin.y))

            scrollView.contentView.layer?.removeAllAnimations()

        }

     }

    @objc func endscroll() {

       print("end")

    

        print(NSApp.currentEvent ?? 0)

        var val = scrollView.contentView.frame.height

        var val2 = scrollView.documentView?.frame.height ?? 0;



        

        if(scrollView.contentView.bounds.origin.y>0 && scrollView.contentView.bounds.origin.y<=(val2-val)-1 ){

            

        var offset = scrollView.contentView.bounds.origin.y

             offset = round(offset / 60) * 60;

        NSAnimationContext.runAnimationGroup({ context in

            context.duration = 2

            crollView.contentView.animator().setBoundsOrigin(NSMakePoint(0, offset))

        }) {

            

        }}

     }

    override func viewDidLoad() {

        super.viewDidLoad()

        scrollView.translatesAutoresizingMaskIntoConstraints = false

        scrollView.borderType = .bezelBorder

        scrollView.backgroundColor = NSColor.gray

        scrollView.hasVerticalScroller = true

        scrollView.hasHorizontalScroller = true

        scrollView.frame.origin.x=300;

        scrollView.frame.origin.y=300;

        self.view.addSubview(scrollView)

        let clipView = NSClipView()

        scrollView.contentView = clipView

        clipView.backgroundColor = NSColor.blue



        // Initial document view

        let documentView = NSView(frame: NSRect(x: 0, y: 0, width: 1200, height: 300))

        documentView.wantsLayer = true

        scrollView.documentView = documentView

        documentView.layer?.backgroundColor = NSColor.red.cgColor

        documentView.layer?.borderWidth = 0

        documentView.layer?.borderColor = NSColor.darkGray.cgColor

        clipView.scaleUnitSquare(to: NSSize(width: 1, height: 1))

        clipView.postsBoundsChangedNotifications = true



        NotificationCenter.default.addObserver(self,

                                               selector: #selector(startscroll),

                                               name: NSScrollView.willStartLiveScrollNotification,

                                               object: scrollView)

  

        NotificationCenter.default.addObserver(self,

                                               selector: #selector(endscroll),

                                               name: NSScrollView.didEndLiveScrollNotification,

                                               object: scrollView)

 

                                                                          

        // Subview1

        let view1 = NSView(frame: NSRect(x: 0, y: 0, width: 1200, height: 60))

        view1.translatesAutoresizingMaskIntoConstraints = false

        view1.wantsLayer = true

        view1.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor

        view1.layer?.borderWidth = 1

        view1.layer?.borderColor = NSColor.gray.cgColor

        documentView.addSubview(view1)

        

        let view2 = NSView(frame: NSRect(x: 0, y: 60, width: 1200, height: 60))

        view2.translatesAutoresizingMaskIntoConstraints = false

        view2.wantsLayer = true

        view2.layer?.backgroundColor = NSColor.green.cgColor

        view2.layer?.borderWidth = 1

        view2.layer?.borderColor = NSColor.gray.cgColor

        documentView.addSubview(view2)

        

        let view3 = NSView(frame: NSRect(x: 0, y: 120, width: 1200, height: 60))

        view3.translatesAutoresizingMaskIntoConstraints = false

        view3.wantsLayer = true

        view3.layer?.backgroundColor = NSColor.green.cgColor

        view3.layer?.borderWidth = 1

        view3.layer?.borderColor = NSColor.gray.cgColor

        documentView.addSubview(view3)

        

        let view4 = NSView(frame: NSRect(x: 0, y: 180, width: 1200, height: 60))

        view4.translatesAutoresizingMaskIntoConstraints = false

        view4.wantsLayer = true

        view4.layer?.backgroundColor = NSColor.green.cgColor

        view4.layer?.borderWidth = 1

        view4.layer?.borderColor = NSColor.gray.cgColor

        documentView.addSubview(view4)

        

        let view5 = NSView(frame: NSRect(x: 0, y: 240, width: 1200, height: 60))

        view5.translatesAutoresizingMaskIntoConstraints = false

        view5.wantsLayer = true

        view5.layer?.backgroundColor = NSColor.green.cgColor

        view5.layer?.borderWidth = 1

        view5.layer?.borderColor = NSColor.gray.cgColor

        documentView.addSubview(view5)



    }

    override func loadView() {

    self.view = NSView(frame: NSRect(x: 0, y: 0, width: NSScreen.main?.frame.width ?? 100, height: NSScreen.main?.frame.height ?? 100))

    }

    override var representedObject: Any? {

        didSet {

        // Update the view, if already loaded.

        }

    }





}


For easier reading I pasted code with Paste And Match Style.

You observe notification at startOfScroll, not during scroll. Is it intentional or should you replace NSScrollView.willStartLiveScrollNotification with NSScrollView.didLiveScrollNotification

import Cocoa

class ViewController: NSViewController {
    
    let scrollView = NSScrollView(frame: NSRect(x: 300, y: 300, width: 600, height: 200))
    
    @objc func startscroll() {
        print("Scroll Started")
        print(NSApp.currentEvent)
        
        var val = scrollView.contentView.frame.height
        var val2 = scrollView.documentView?.frame.height ?? 0;
        
        if (scrollView.contentView.bounds.origin.y>0 && scrollView.contentView.bounds.origin.y<=(val2-val)-1 ){
            scrollView.contentView.layer?.removeAllAnimations()
        }
        
    }
    
    @objc func endscroll() {
        print("end")
        
        print(NSApp.currentEvent ?? 0)
        
        var val = scrollView.contentView.frame.height
        var val2 = scrollView.documentView?.frame.height ?? 0;
        
        if (scrollView.contentView.bounds.origin.y>0 && scrollView.contentView.bounds.origin.y<=(val2-val)-1 ){
            
            var offset = scrollView.contentView.bounds.origin.y
            offset = round(offset / 60) * 60;
            NSAnimationContext.runAnimationGroup({ context in
                context.duration = 2
                scrollView.contentView.animator().setBoundsOrigin(NSMakePoint(0, offset))
            }) {
            }}
        
    }
    
    override func viewDidLoad() {
        
        super.viewDidLoad()
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.borderType = .bezelBorder
        scrollView.backgroundColor = NSColor.gray
        scrollView.hasVerticalScroller = true
        scrollView.hasHorizontalScroller = true
        scrollView.frame.origin.x=300;
        scrollView.frame.origin.y=300;
        self.view.addSubview(scrollView)
        
        let clipView = NSClipView()
        scrollView.contentView = clipView
        clipView.backgroundColor = NSColor.blue
        
        // Initial document view
        
        let documentView = NSView(frame: NSRect(x: 0, y: 0, width: 1200, height: 300))
        documentView.wantsLayer = true
        
        scrollView.documentView = documentView
        documentView.layer?.backgroundColor = NSColor.red.cgColor
        documentView.layer?.borderWidth = 0
        documentView.layer?.borderColor = NSColor.darkGray.cgColor
        
        clipView.scaleUnitSquare(to: NSSize(width: 1, height: 1))
        clipView.postsBoundsChangedNotifications = true
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(startscroll),
            name: NSScrollView.willStartLiveScrollNotification,
            object: scrollView)
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(endscroll),
            name: NSScrollView.didEndLiveScrollNotification,
            object: scrollView)
        
        // Subview1
        
        let view1 = NSView(frame: NSRect(x: 0, y: 0, width: 1200, height: 60))
        view1.translatesAutoresizingMaskIntoConstraints = false
        view1.wantsLayer = true
        view1.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
        view1.layer?.borderWidth = 1
        view1.layer?.borderColor = NSColor.gray.cgColor
        
        documentView.addSubview(view1)
        
        let view2 = NSView(frame: NSRect(x: 0, y: 60, width: 1200, height: 60))
        view2.translatesAutoresizingMaskIntoConstraints = false
        view2.wantsLayer = true
        view2.layer?.backgroundColor = NSColor.green.cgColor
        view2.layer?.borderWidth = 1
        view2.layer?.borderColor = NSColor.gray.cgColor
        
        documentView.addSubview(view2)
        
        let view3 = NSView(frame: NSRect(x: 0, y: 120, width: 1200, height: 60))
        view3.translatesAutoresizingMaskIntoConstraints = false
        view3.wantsLayer = true
        view3.layer?.backgroundColor = NSColor.green.cgColor
        view3.layer?.borderWidth = 1
        view3.layer?.borderColor = NSColor.gray.cgColor
        
        documentView.addSubview(view3)
        
        let view4 = NSView(frame: NSRect(x: 0, y: 180, width: 1200, height: 60))
        view4.translatesAutoresizingMaskIntoConstraints = false
        view4.wantsLayer = true
        view4.layer?.backgroundColor = NSColor.green.cgColor
        view4.layer?.borderWidth = 1
        view4.layer?.borderColor = NSColor.gray.cgColor
        
        documentView.addSubview(view4)
        
        let view5 = NSView(frame: NSRect(x: 0, y: 240, width: 1200, height: 60))
        view5.translatesAutoresizingMaskIntoConstraints = false
        view5.wantsLayer = true
        view5.layer?.backgroundColor = NSColor.green.cgColor
        view5.layer?.borderWidth = 1
        view5.layer?.borderColor = NSColor.gray.cgColor
        
        documentView.addSubview(view5)
        
    }
    
    override func loadView() {
        
        self.view = NSView(frame: NSRect(x: 0, y: 0, width: NSScreen.main?.frame.width ?? 100, height: NSScreen.main?.frame.height ?? 100))
        
    }
    
    override var representedObject: Any? {
        
        didSet {
            
            // Update the view, if already loaded.
            
        }
        
    }
    
}

Thanks it seems like I solved the issue. So scrollView.contentView.layer?.removeAllAnimations() doesn't do anything, maybe someone knows what I misunderstood there. BUT: I can create a new animation group, which also needs a very short duration (maybe also 0), so it seems like if I set just ScrollView.contentView.animator().setBoundsOrigin(NSMakePoint(0, scrollView.contentView.bounds.origin.y)) it is still using the set 2 second animation interval. Therefore setting the context duration to 0.0 will solve the issue:

 NSAnimationContext.runAnimationGroup({ context in

              context.duration = 0.00

              scrollView.contentView.animator().setBoundsOrigin(NSMakePoint(0, scrollView.contentView.bounds.origin.y))

          }) {

              

          }

Great you found a fix.

  • But with duration of 0.0, what is the interest of running animation ?
  • You should get the same result by calling directly setBoundsOrigin(NSMakePoint(0, scrollView.contentView.bounds.origin.y))
  • I was also wondering what do you expect by animating the bounds origin ?

I remember I once had problems to stop Animations with removeAllAnimations.

The case:

  • animating a window resize by setting its frame.
  • the button (inside the view that was animated) that was supposed to stop it did react (highlighted) but did not execute apparently.

I found out that it was as if the animator created a new view and was performing animation on it ; hence the removeAllAnimations was not sent to the right one. But if animation was to set alpha of a label, it could be interrupted by the button action (button was not included in the animation).

Maybe (I'm not sure) animating the bounds create a similar issue.

  • Removed - Duplicate answer
NSScrollview - Pagination effect - Stop animation
 
 
Q