How to make NSView draggable in Swift (OSX Mac app)? - swift

I'm trying to make two NSView's draggable and I was wondering how to code that in Swift for OSX Mac app? I'm thinking to put two NSView's within an NSView and make them draggable within that NSView. For example, create NSViewA and nested within in are NSViewB and NSViewC (both draggable within NSViewA). I would like to code in Swift. Cheers!

You can do this fairly easily if you take advantage of the information passed on the responder chain. Consider this custom NSView class.
class DraggableView : NSView {
var startPoint: NSPoint?
var frameOrigin: NSPoint?
override func mouseDown(with event: NSEvent) {
startPoint = event.locationInWindow
frameOrigin = frame.origin
}
override func mouseDragged(with event: NSEvent) {
let offset = event.locationInWindow - startPoint!
frame.origin = frameOrigin! + offset
}
override func mouseUp(with event: NSEvent) {
startPoint = nil
frameOrigin = nil
}
}
To make the math work for NSPoint, I overloaded operators in the extension:
extension NSPoint {
static func -(pointA: NSPoint, pointB: NSPoint) -> NSPoint {
return NSPoint(x: pointA.x - pointB.x, y: pointA.y - pointB.y)
}
static func +(pointA: NSPoint, pointB: NSPoint) -> NSPoint {
return NSPoint(x: pointA.x + pointB.x, y: pointA.y + pointB.y)
}
}
Then your ViewController class is just setup code:
class ViewController: NSViewController {
let viewA: DraggableView = DraggableView()
let viewB: DraggableView = DraggableView()
override func viewDidLoad() {
super.viewDidLoad()
viewA.frame = NSRect(origin: .zero, size: NSSize(width: 100, height: 100))
viewB.frame = NSRect(origin: NSPoint(x: 125, y: 0), size: NSSize(width: 100, height: 100))
viewA.wantsLayer = true
viewB.wantsLayer = true
viewA.layer?.backgroundColor = NSColor.blue.cgColor
viewB.layer?.backgroundColor = NSColor.green.cgColor
view.addSubview(viewA)
view.addSubview(viewB)
}
}

Related

drawRect function not triggered when needDisplay is called

I am trying to create a custom class to build various graph views for the app eventually. The main class is the viewController class where I would like to present the graphs managed by the clGraph class. clGraph class will be used to process the data and create a graph view by vGraph class.
plotGraph function in the vGraphs is where I am struggling. It appears that the drawRect function is not called, however, setNeedsDisplay is called. Below is the test code that I have to understand and learn the basics. Requesting help to be able to draw a basic line on the targetView. Thank you.
class main : NSViewController {
#IBOutlet weak var targetView: NSView!
func plotGraph(){
let g = clGraphs()
g.initialize(graphCanvas: targetView, controller: self)
g.processData()
g.plotline()
}
}
class clGraphs {
var controller = NSViewController()
var graphCanvas = NSView()
var lineData = [Float]()
public func initialize (graphCanvas : NSView, controller : NSViewController) -> clGraphs{
self.graphCanvas = graphCanvas
self.controller = controller
return self
}
public func plotline(){
let lb = NSTextField(frame: NSRect(x: 10, y: 10, width: 100, height: 20))
lb.stringValue = "Test" //works fine on the view
graphCanvas.addSubview(lb)
print(graphCanvas.frame) //works fine
let g = vGraphs()
g.graphCanvas = graphCanvas
g.needsDisplay = true
}
public func processData(){
//Some logic to update lineData
}
}
class vGraphs: NSView{
var graphCanvas = NSView()
var lineData = [Float]()
override func draw(_ dirtyRect: NSRect) {
print("In the draw rect function")
}
override func setNeedsDisplay(_ invalidRect: NSRect) {
print("In the set needs display function")
plotGraph()
}
func plotGraph(){
graphCanvas.layer?.backgroundColor = NSColor.blue.cgColor //works fine
let aPath = NSBezierPath()
aPath.move(to: NSPoint(x: 50, y: 50))
aPath.line(to: NSPoint(x: 200, y: 200))
aPath.lineWidth = 10
aPath.close()
aPath.stroke()
// ERROR: CGContextRestoreGState: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.
}
}

Problem combining a draggable NSView with SwiftUI components

I have a NSView-class written using Cocoa:
import Cocoa
import SwiftUI
final class EntityViewImpl: NSViewRepresentable {
func makeNSView(context: NSViewRepresentableContext<EntityViewImpl>) -> EntityView {
return EntityView(frame: NSRect(x: 0, y: 0, width: 100, height: 200))
}
func updateNSView(_ nsView: EntityView, context: NSViewRepresentableContext<EntityViewImpl>) {
// do nothing
}
}
class EntityView: NSView {
var oldMousePosition: NSPoint = .zero
var oldFrameOrigin: NSPoint = .zero
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
NSColor.white.setFill()
dirtyRect.fill()
}
override func mouseDown(with event: NSEvent) {
self.oldFrameOrigin = self.frame.origin
self.oldMousePosition = event.locationInWindow
}
override func mouseDragged(with event: NSEvent) {
let pointInSuperview = event.locationInWindow
self.frame.origin = NSPoint(x: oldFrameOrigin.x + (pointInSuperview.x - oldMousePosition.x),
y: oldFrameOrigin.y + (pointInSuperview.y - oldMousePosition.y))
}
}
and I want to use this class inside a SwiftUI View-body like:
struct ContentView: View {
var body: some View {
Group {
EntityViewImpl()
.frame(width: 100, height: 100)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
This works fine and I can also drag around the EntityView. However, I can only "grab" it inside of it's original frame. If I drag it outside of the bounds, the mouse event will no longer trigger on the parts of the view that are outside of the original frame. Do I have to somehow "tell" the ContentView that the NSView moved?
Actually if you are using SwiftUI, you need to move the superview of EntityView, like this:
override func mouseDown(with event: NSEvent) {
self.oldFrameOrigin = self.superview!.frame.origin
self.oldMousePosition = self.superview!.convert(event.locationInWindow, from: self)
}
override func mouseDragged(with event: NSEvent) {
let pointInSuperview = self.superview!.convert(event.locationInWindow, from: self)
self.superview!.frame.origin = NSPoint(x: oldFrameOrigin.x + (pointInSuperview.x - oldMousePosition.x),
y: oldFrameOrigin.y + (-pointInSuperview.y + oldMousePosition.y))
}

Drag a CGRect using UIPanGestureRecognizer

I am subclassing UIView, and am trying to "drag" a CGRect back and forth across the screen. Basically I want to move(redraw) the rectangle every time I drag my finger. So far, I have this code:
var rectangle: CGRect {
get {
return CGRect(x: 200,
y: 200,
width: frame.width / 6,
height: 15)
}
set {}
}
override func draw(_ rect: CGRect) {
let gesture = UIPanGestureRecognizer(target: self, action: #selector(dragRectangle(recognizer:)))
addGestureRecognizer(gesture)
drawRectangle(rect)
}
func drawRectangle(_ rect: CGRect) {
let path = UIBezierPath(rect: rectangle)
UIColor.black.set()
path.fill()
}
#objc func dragRectangle(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self)
rectangle = CGRect(x: rectangle.midX + translation.x, y: rectangle.midY + translation.y, width: rectangle.width, height: rectangle.height)
setNeedsDisplay()
recognizer.setTranslation(CGPoint.zero, in: self)
}
This is my first time using UIPanGestureRecognizer, so I'm not sure of all the details that go into this. I have set breakpoints in drawRectangle and confirmed that this is being called. However, the rectangle on the screen does not move at all, no matter how many times I try to drag it. What's wrong?
This is how you can do it easily. just copy paste and run this.
//
// RootController.swift
// SampleApp
//
// Created by Chanaka Caldera on 24/6/19.
// Copyright © 2019 homeapps. All rights reserved.
//
import UIKit
class RootController: UIViewController {
private var draggableView: UIView!
private var pangesture: UIPanGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
setdragview()
}
}
extension RootController {
fileprivate func setdragview() {
draggableView = UIView()
draggableView.translatesAutoresizingMaskIntoConstraints = false
draggableView.backgroundColor = .lightGray
view.addSubview(draggableView)
let draggableviewConstraints = [draggableView.leftAnchor.constraint(equalTo: view.leftAnchor,constant: 10),
draggableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 64),
draggableView.widthAnchor.constraint(equalToConstant: 100),
draggableView.heightAnchor.constraint(equalToConstant: 40)]
NSLayoutConstraint.activate(draggableviewConstraints)
pangesture = UIPanGestureRecognizer()
draggableView.isUserInteractionEnabled = true
draggableView.addGestureRecognizer(pangesture)
pangesture.addTarget(self, action: #selector(draggableFunction(_:)))
}
}
extension RootController {
#objc fileprivate func draggableFunction(_ sender: UIPanGestureRecognizer) {
view.bringSubviewToFront(draggableView)
let translation = sender.translation(in: self.view)
draggableView.center = CGPoint(x: draggableView.center.x + translation.x, y: draggableView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
print("drag works : \(translation.x)")
}
}
here is the demo,
Hope this will help. cheers !
Try like this (check comments through code):
#IBDesignable
class Rectangle: UIView {
#IBInspectable var color: UIColor = .clear {
didSet { backgroundColor = color }
}
// draw your view using the background color
override func draw(_ rect: CGRect) {
backgroundColor?.set()
UIBezierPath(rect: rect).fill()
}
// add the gesture recognizer to your view
override func didMoveToSuperview() {
addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(pan)))
}
// your gesture selector
#objc func pan(_ gesture: UIPanGestureRecognizer) {
// update your view frame origin
frame.origin += gesture.translation(in: self)
// reset the gesture translation
gesture.setTranslation(.zero, in: self)
}
}
extension CGPoint {
static func +=(lhs: inout CGPoint, rhs: CGPoint) {
lhs.x += rhs.x
lhs.y += rhs.y
}
}
To draw rectangles on your view when panning you can do as follow:
import UIKit
class ViewController: UIViewController {
var rectangles: [Rectangle] = []
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(pan)))
}
#objc func pan(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
let rectangle = Rectangle(frame: .init(origin: gesture.location(in: view), size: .init(width: 0, height: 0)))
rectangle.fillColor = .red
rectangle.strokeColor = .white
rectangle.lineWidth = 3
view.addSubview(rectangle)
rectangles.append(rectangle)
case .changed:
let distance = gesture.translation(in: view)
let index = rectangles.index(before: rectangles.endIndex)
let frame = rectangles[index].frame
rectangles[index].frame = .init(origin: frame.origin, size: .init(width: frame.width + distance.x, height: frame.height + distance.y))
rectangles[index].setNeedsDisplay()
gesture.setTranslation(.zero, in: view)
case .ended:
break
default:
break
}
}
}
Sample Project
I figured out most of the problem as to why the rectangle wasn't moving. It turns out that I misunderstood how variable getters and setters work in Swift. Nevertheless, I changed this code:
var rectangle: CGRect {
get {
return CGRect(x: 200,
y: 200,
width: frame.width / 6,
height: 15)
}
set {}
}
to be a lazy variable:
lazy var rectangle: CGRect = {
return CGRect(x: 200,
y: 200,
width: self.frame.width / 6,
height: 15)
}()
The reason I needed get and set in the first place was because I use frame in my variable calculations, and that was not available until the view itself was fully instantiated. I also tweaked this code a bit:
rectangle = CGRect(x: rectangle.midX + translation.x, y: rectangle.midY + translation.y, width: rectangle.width, height: rectangle.height)
and used minX instead of midX:
rectangle = CGRect(x: rectangle.minX + translation.x, y: rectangle.minY + translation.y, width: rectangle.width, height: rectangle.height)
This is because CGRect is initialized with the x and y parameters being the minX and minY.
This is good progress(at least the rectangle moves). However, I am not able to figure out why the rectangle only switches places after I have released my mouse, resulting in choppy movement.

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)
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
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")
}
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

How to draw a line between two views in Swift 3?

I need help with drawing a simple line between two Outline Views in Swift 3 (Xcode 8).
My situation:
Main ViewController
|--- Main View
|--- Outline View
|--- Outline View
So I Need help to get the coordinates of both Outline Views and draw a line with them (the line itself is not that difficult, more to get the coordinates). The goal is to draw a line (programmatically) that connects both Outline Views (f.ex. from one edge to the other, or from the top, ...).
I already tried following:
class Line: NSView{
var origin = CGPoint()
var destination = CGPoint()
required init?(coder aDecoder: NSCoder){
super.init(coder: aDecoder)
}
init(fromPoint: CGPoint, toPoint: CGPoint){
self.origin = fromPoint
self.destination = toPoint
super.init(frame: CGRect(origin: fromPoint, size: CGSize(width: destination.x - origin.x, height: destination.y - origin.y)))
}
override func draw(_ dirtyRect: NSRect){
let myPath = NSBezierPath()
myPath.move(to: CGPoint(x: origin.x, y: origin.y))
myPath.line(to: CGPoint(x: destination.x - origin.x, y: destination.y - origin.y))
myPath.stroke()
}
}
class ViewController: NSViewController{
override func viewDidLoad(){
super.viewDidLoad()
let line = Line(fromPoint: self.view.convert(CGPoint.zero, to: self.view.viewWithTag(1)), toPoint: self.view.convert(CGPoint.zero, to: self.view.viewWithTag(2)))
view.addSubview(line)
}
}
But that didn't do anything.
I would appreciate your help!
Thank you
I now solved my problem (more or less) as following:
class Line: NSView{
var fromPoint = CGPoint()
var toPoint = CGPoint()
func setPoints(fromPoint: CGPoint, toPoint: CGPoint){
self.fromPoint = fromPoint
self.toPoint = toPoint
}
override func draw(_ dirtyRect: NSRect) {
let path = NSBezierPath()
NSColor.green.setFill()
path.move(to: fromPoint)
path.line(to: toPoint)
path.stroke()
}
}
class ViewController: NSViewController{
override function viewDidLoad(){
super.viewDidLoad()
let subview3 = Line(frame: self.view.bounds)
subview3.setPoints(fromPoint: subview1.convert(CGPoint(x: subview1.bounds.maxX, y: subview1.bounds.maxY), to: self.view), toPoint: subview2.convert(CGPoint(x: subview2.bounds.minX, y: subview2.bounds.minY), to: self.view))
self.view.addSubview(subview3)
}
}
I need to know how to do this on runtime. Do I always have to create a new view in order to draw a path?
A full example:
//
// ViewController.swift
// DrawConnectViews
//
// Created by T M on 17.06.17.
// Copyright © 2017 TM. All rights reserved.
//
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let subview1 = CustomViewWithColor(frame: NSRect(origin: CGPoint(x: 10.0, y: 10.0), size: CGSize(width: 200.0, height: 200.0)))
let subview2 = CustomViewWithColor(frame: NSRect(origin: CGPoint(x: 360.0, y: 360.0), size: CGSize(width: 200.0, height: 200.0)))
// create a subview programatically:
let subview3 = Line(frame: self.view.bounds)
subview3.setPoints(fromPoint: subview1.convert(CGPoint(x: subview1.bounds.maxX, y: subview1.bounds.maxY), to: self.view), toPoint: subview2.convert(CGPoint(x: subview2.bounds.minX, y: subview2.bounds.minY), to: self.view))
self.view.addSubview(subview3)
subview1.setColor(color: NSColor.red)
subview2.setColor(color: NSColor.blue)
self.view.addSubview(subview1)
self.view.addSubview(subview2)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
class CustomViewWithColor: NSView{
var color = NSColor()
func setColor(color: NSColor){
self.color = color
}
override func draw(_ dirtyRect: NSRect) {
let path = NSBezierPath(rect: self.bounds)
self.color.setFill()
path.fill()
}
}
class Line: NSView{
var fromPoint = CGPoint()
var toPoint = CGPoint()
func setPoints(fromPoint: CGPoint, toPoint: CGPoint){
self.fromPoint = fromPoint
self.toPoint = toPoint
}
override func draw(_ dirtyRect: NSRect) {
let path = NSBezierPath()
NSColor.green.setFill()
path.move(to: fromPoint)
path.line(to: toPoint)
path.stroke()
}
}
That produces following:
Output of program
A lot of people will think that this is overkill, but what I do is add a subview whose background is the line color, whose height is constrained to the desired line thickness, and whose leading and trailing edges are constrained to the superview's leading and trailing edges. This ensures that the border will always adjust with the superview's size, which is why adding a border as a layer or customizing the view's draw(in:) to draw the border as a path don't work as well.