'How to move NSImageView inside NSView in Cocoa?

In my Mac OS app I need a functionality to be able to move NSImageView around inside an NSView after mouseDown event (triggered by user) happen in this NSImageView. When the user triggers mouse Up event, this view must move to the last mouseDrag event direction and stay there. During the move I want the NSImageView to be visible on the screen (it should move along with the mouse cursor).

I've read a Handling Mouse Events guide by Apple, https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/EventOverview/HandlingMouseEvents/HandlingMouseEvents.html Also I've downloaded this sample code: https://developer.apple.com/library/content/samplecode/DragItemAround/Introduction/Intro.html

Both links contain code in Objective C. Code for DragItemAround is outdated. I've tried to search for solutions on GitHub and other StackOverflow threads, but have not found any working solutions.

Would be glad to hear the answers on this question. I'm using Swift 3.

I've created a custom MovableImageView which is a subclass of NSImageView with this code:

import Cocoa

class MovableImageView: NSImageView {

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)

        // Drawing code here.
        // setup the starting location of the
        // draggable item

    }

    override func mouseDown(with event: NSEvent) {
        Swift.print("mouseDown")

        //let window = self.window!
        //let startingPoint = event.locationInWindow


    }


    override func mouseDragged(with event: NSEvent) {
        Swift.print("mouseDragged")

        self.backgroundColor = NSColor.black

    }

    override func mouseUp(with event: NSEvent) {
        Swift.print("mouseUp")
    }

}

After that in Interface Builder i've set this class to NSImageView. I've also set constraints in Interface Builder for this NSImageView.

Now i'm trying to figure out how to move NSImageView inside NSView? (Swift 3, XCode 8)



Solution 1:[1]

You need to save the mouseDown point and use it for offset later. Please check the following code:

class MovableImageView: NSImageView {

var firstMouseDownPoint: NSPoint = NSZeroPoint

init() {
    super.init(frame: NSZeroRect)
    self.wantsLayer = true
    self.layer?.backgroundColor = NSColor.red.cgColor
}

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

override func draw(_ dirtyRect: NSRect) {
    super.draw(dirtyRect)

    // Drawing code here.
}

override func mouseDown(with event: NSEvent) {
    Swift.print("mouseDown")
    firstMouseDownPoint = (self.window?.contentView?.convert(event.locationInWindow, to: self))!
}

override func mouseDragged(with event: NSEvent) {
    Swift.print("mouseDragged")
    let newPoint = (self.window?.contentView?.convert(event.locationInWindow, to: self))!
    let offset = NSPoint(x: newPoint.x - firstMouseDownPoint.x, y: newPoint.y - firstMouseDownPoint.y)
    let origin = self.frame.origin
    let size = self.frame.size
    self.frame = NSRect(x: origin.x + offset.x, y: origin.y + offset.y, width: size.width, height: size.height)
}

override func mouseUp(with event: NSEvent) {
    Swift.print("mouseUp")

    }
}

In the parent view, just add this MovableImageView as subview like this:

let view = MovableImageView()
view.frame = NSRect(x: 0, y: 0, width: 100, height: 100)
self.view.addSubview(view) //Self is your parent view

enter image description here

Solution 2:[2]

Constraint version of @brianLikeApple's answer:

class MovableImageView: NSImageView {

var firstMouseDownPoint: NSPoint = NSZeroPoint
var xConstraint: NSLayoutConstraint!
var yContstraint: NSLayoutConstraint!

required init?(coder aDecoder: NSCoder) {
   super.init(coder: aDecoder)
    self.wantsLayer = true
    self.layer?.backgroundColor = NSColor.red.cgColor
}
override func draw(_ dirtyRect: NSRect) {
    super.draw(dirtyRect)
    
    // Drawing code here.
}

override func mouseDown(with event: NSEvent) {
    Swift.print("mouseDown")
    firstMouseDownPoint = (self.window?.contentView?.convert(event.locationInWindow, to: self))!
}

override func mouseDragged(with event: NSEvent) {
    Swift.print("mouseDragged")
    let newPoint = (self.window?.contentView?.convert(event.locationInWindow, to: self))!
    let offset = NSPoint(x: newPoint.x - firstMouseDownPoint.x, y: newPoint.y - firstMouseDownPoint.y)
    let origin = self.frame.origin
    let size = self.frame.size
    yContstraint.constant = (self.superview?.frame.height)! - origin.y - offset.y - size.height
    xConstraint.constant = origin.x + offset.x
}

override func mouseUp(with event: NSEvent) {
    Swift.print("mouseUp")
    
}

Solution 3:[3]

Here's a NSView you can drag. It uses constraints exactly like @ExeRhythm answer.

import AppKit
class DraggableNSView: NSView {
    var down: NSPoint = NSZeroPoint
    @IBOutlet var across: NSLayoutConstraint!
    @IBOutlet var up: NSLayoutConstraint!

    override func mouseDown(with e: NSEvent) {
        down = window?.contentView?.convert(e.locationInWindow, to: self) ?? NSZeroPoint
    }

    override func mouseDragged(with e: NSEvent) {
        guard let m = window?.contentView?.convert(e.locationInWindow, to: self) else { return }
        let p = frame.origin + (m - down)
        across.constant = p.x
        up.constant = p.y
    }
}

Don't forget that you must connect the across and up constraints in storyboard!

Note. If you prefer a "down" constraint rather than "up", it is:

        // if you prefer a "down" constraint:
        down.constant = (superview?.frame.height)! - p.y - frame.size.height

As always, add these two functions in your project,

func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
    return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}

func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
    return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}

Note that you can of course do this, to any subclass of NSView such as NSImageView:

class DraggableNSImageView: NSView {
     ... same

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 ExeRhythm
Solution 3 Fattie