Search code examples
macoscalayernsviewnsscrollview

CALayer with NSScrollView, zooming panning and clicking


I have a CALayer hosting view, which I would like to be able to zoom, scroll around and click in. If I embed it in a scrollview, the scrolling works fine, but zooming causes the contents to fly away everywhere. If I don't embed it in a scrollview, I can get the zooming working nicely using CATransform3DMakeScale, but then I have problems with panning, and being able to select objects.

What would be the recommended way to approach this - before I try and work it out with the wrong option.

(If you have any source that does a similar thing, I would be very grateful to see it.)


Solution

  • I got inspired here at Apple sample codes

    Also look at the Optimizing Drawing and Scrolling on OS X WWDC 2013 video

    The CALayer embedded in ScrollView flown because the ScrollView wasn't the Core Animation layer (ScrollView.wantsLayer = true). Interface Builder

    Here example in Swift:

    @NSApplicationMain
    class AppDelegate: NSObject, NSApplicationDelegate {
        var win: NSWindow!
    
        func applicationDidFinishLaunching(aNotification: NSNotification) {
    
            win = NSWindow(contentRect: NSRect(x: 200, y: 200, width: 500, height: 500),
                styleMask: NSTitledWindowMask,
                backing: NSBackingStoreType.Buffered,
                defer: false)
    
            win.makeKeyAndOrderFront(win)
    
            let scrollView = NSScrollView()
            scrollView.allowsMagnification = true
            scrollView.hasHorizontalScroller = true
            scrollView.hasVerticalScroller = true
            scrollView.wantsLayer = true
    
            scrollView.contentView = CenteredClipView()
            scrollView.documentView = MyView()
            scrollView.backgroundColor = NSColor.controlColor()
    
            win.contentView = scrollView
        }
    }
    
    class MyView: NSView {
        var drawn = false
    
        override func updateLayer() {
            super.updateLayer()
    
            if !drawn {
                drawn = true
    
                frame = (superview! as NSView).frame
    
                var shape = CAShapeLayer()
                let p = CGPathCreateMutable()
                CGPathAddEllipseInRect(p, nil, frame)
                CGPathMoveToPoint(p, nil, bounds.maxX, 0)
                CGPathAddLineToPoint(p, nil, 0, bounds.maxY)
                shape.path = p
                shape.lineWidth = 5
                shape.fillColor = NSColor.whiteColor().CGColor
                shape.strokeColor = NSColor.selectedControlColor().CGColor
                shape.lineDashPattern = [5, 5]
    
                let ant = CABasicAnimation(keyPath: "lineDashPhase")
                ant.fromValue = 0
                ant.toValue = 1000000
                ant.duration = 10000
                ant.repeatCount = 10000
                shape.addAnimation(ant, forKey: "lineDashPhase")
    
                layer?.addSublayer(shape)
            }
        }
    }
    
    class CenteredClipView: NSClipView {
        override func constrainBoundsRect(proposedBounds: NSRect) -> NSRect {
            var rect = super.constrainBoundsRect(proposedBounds)
    
            if let containerView = documentView? as? NSView {
                if rect.size.width > containerView.frame.size.width {
                    rect.origin.x = (containerView.frame.width - rect.width ) / 2
                }
    
                if rect.size.height > containerView.frame.size.height {
                    rect.origin.y = (containerView.frame.height - rect.height ) / 2
                }
            }
    
            return rect
        }
    }