Draw a graph in a NSView using values entered by the user - swift

I'm trying to draw a graph using Swift. I have created a custom NSView and a NSView class.
In the ViewController I would like to call a method that updates the NSView and draws the complete graph using values entered by the user.
import Cocoa
class Graph: NSView {
let startPoint = NSPoint(x: 10, y: 10)
override func drawRect(dirtyRect: NSRect) {
NSColor.whiteColor().set() // set white color
NSRectFill(dirtyRect) // fill the Rect in the View
// draw axes lines
NSColor.blackColor().set()
NSBezierPath.strokeLineFromPoint(startPoint, toPoint: NSPoint(x: Double(dirtyRect.width) - 10.0, y: 10.0))
NSBezierPath.strokeLineFromPoint(startPoint, toPoint: NSPoint(x: 10.0, y: Double(dirtyRect.height) - 10.0))
}
func drawGraphicsOfNewteork(arrayOfData: [NSTextField]){
// Here I would draw in the View some lines using the arrayOfData
}
}

Have you read the documentation at all? Searching for "drawing" turns up the Cocoa Drawing Guide.
As others have stated, drawing is done from within drawRect(). The system will call it at the "right time", after you flag it by setting needsDisplay to true/YES.

Related

Draw Trails in Xcode

I am working on a screensaver in Xcode. I have an array of points and am changing their position s every frame in the override func animateOneFrame(). How would I have the points create a fading trail similar to this: https://www.youtube.com/watch?v=fDSIRXmnVvk&t=117s&ab_channel=CodeParade
For reference, in my draw function I have this and it only displays the points, no trail. It effectively draws a black background every frame even though I don't tell it to:
override func draw(_ rect: NSRect) {
for point in 1..<PointArray.count {
drawPoint(p: PointArray[point])
}
}
private func drawPoint(p: point) {
let pixel = NSRect(x: p.position.x,
y: p.position.y,
width: 3,
height: 3)
p.color.setFill()
pixel.fill()
}

Swift: NSBezier

This is the code inside my custom view class:
func drawTestingPoint(_ point: CGPoint, target: Int, output: Int) {
let path = NSBezierPath()
path.appendArc(withCenter: point, radius: 5, startAngle: 0, endAngle: 360)
NSColor.black.setStroke()
if target == output {
NSColor.green.setFill()
} else {
NSColor.red.setFill()
}
path.lineWidth = 3
path.fill()
path.stroke()
}
override func draw(_ dirtyRect: NSRect) {
//If I call the drawTestingPoint function here it works
}
Inside my viewDidLoad method in my NSViewController class I set up the custom view and try to draw the testing point:
let size = getDataViewSize()
let origin = CGPoint(x: view.frame.width/2-size.width/2, y: view.frame.height/2-size.height/2)
dataView = DataView(frame: CGRect(origin: origin, size: size))
view.addSubview(dataView)
dataView.drawTestingPoint(CGPoint(x: view.frame.width/2 y: view.frame.height/2), target: target, output: output)
dataView.needsDisplay = true
My problem is that no point is getting drawn. I think there can't be anything wrong with my drawTestingPoint function because when I call it inside my draw(_ dirtyRect: NSRect) function in my custom NSView class, it works. What can I do so I can call this function inside my viewDidLoad function how you can see in the codes snippets above so my point gets drawn
You can't just draw any time you want. Normally you set up a view and implement draw(_:) as you've done. The system calls the draw method when it needs the view to draw its contents. Before calling your draw(_:) method it sets up the drawing context correctly to draw inside your view and clip if you draw outside of the view. That's the bit you're missing.
As a general rule you should NOT draw outside of the view's draw(_:) method. I've done drawing outside of the draw(_:) method so infrequently that I don't remember what you'd need to do to set up the drawing context correctly. (To be fair I do mostly iOS development these days and my MacOS is getting rusty.)
So the short answer is "Don't do that."
EDIT:
Instead, set up your custom view to save the information it needs to draw itself. As others have suggested, when you make changes to the view, set needsDisplay=true on the view. That will cause the system to call the view's draw(_:) method on the next pass through the event loop

Colored text background with rounded corners

I'm currently working on an iOS application in Swift, and I need to achieve the text effect shown in the attached picture.
I have a label which displays some text wrote by the user, and I need to make the text background corners (not the label background) rounded.
Is there a way to do that?
I'd search the web and Stackoverflow but with no luck.
Thank you.
Here is some code that could help you. The result I got is quite similar to what you want.
class MyTextView: UITextView {
let textViewPadding: CGFloat = 7.0
override func draw(_ rect: CGRect) {
self.layoutManager.enumerateLineFragments(forGlyphRange: NSMakeRange(0, self.text.count)) { (rect, usedRect, textContainer, glyphRange, Bool) in
let rect = CGRect(x: usedRect.origin.x, y: usedRect.origin.y + self.textViewPadding, width: usedRect.size.width, height: usedRect.size.height*1.2)
let rectanglePath = UIBezierPath(roundedRect: rect, cornerRadius: 3)
UIColor.red.setFill()
rectanglePath.fill()
}
}
}
Xavier solution works great.
If you want update background in real-time, use: textView.setNeedsDisplay()

Transform a view by dragging its edge (Swift)

I try to make an app where the user can draw or add an arrow, and transform this arrow (translate, rotate, ...).
For the moment, I manage to draw the arrow and make all the transformations on it, but what I would like to do now is to be able to modify the arrow by dragging its edges.
To draw an arrow, I just create a UIView with a height of 20px (it is the thickness of the arrow), and a width of 400 (length of the arrow).
func drawArrow(frame: CGRect) {
let thicknessArrow = 20
let viewHorizontalArrow = UIView()
viewHorizontalArrow.frame.origin = CGPoint(x: 100, y: 600)
viewHorizontalArrow.frame.size = CGSize(width: 400, height: thicknessArrow)
drawDoubleArrow(viewHorizontalArrow, startPoint: CGPoint(x: 0, y: thicknessViewWithArrow/2), endPoint: CGPoint(x: viewHorizontalArrow.frame.width, y: thicknessViewWithArrow/2), lineWidth: 10, color: UIColor.blackColor())
}
After that, I transform the UIView thanks to the pan, pinch and rotate gesture.
The function "drawDoubleArrow" create an arrow with BezierPath and add it to the layer of the UIView.
I hope these explanations are clear enough :).
Could you help me to find a solution ?
Thanks !
UIBezierPath on it's own cannot easily be manipulated by gestures because it doesn't recognize them. But a UIView can recognize gestures. Combined with a CALayer's ability to be transformed and it's hitTest() method, I think you can achieve what you want.
// I'm declaring the layer as a shape layer, but depending on how your error thick is, you may need a rectangular one
let panGesture = UIPanGestureRecognizer()
var myLayer = CAShapeLayer()
viewDidLoad:
// I'm assuming you've created myBezierPath, an arrow path
myLayer.path = myBezierPath.cgPath
// Adding the pan gesture to the view's controller
panGesture.addTarget(self, action: #selector(moveLayer))
self.addGestureRecognizer(panGesture)
moveLayer:
func moveLayer(_ recognizer:UIPanGestureRecognizer) {
let p = recognizer.location(in: self)
// You will want to loop through all the layers you wish to transform
if myLayer.hitTest(p) != nil {
myLayer.position = p
}
}

Swift 3 - Custom UIButton radius deletes constraints?

My view controller possesses a stack view with 3 buttons. One fixed in the center with a fixed width and the stackview set to fill proportionally such that the other two buttons will be symmetrical.
However I am also customizing the corner radius of the buttons and as soon as the application loads the button resizes in an undesired fashion.
Ive attempted numerous stackview distribution and fill settings. Removing the buttons from the stackview and simply trying to contraint them to edges on a normal UIView to no avail as it seems, but uncertain if, the constraints get deleted.
Visually the button will be located at the bottom right hand corner of the screen with 0 space between the edge and the button. Currently it gets laid out in a manner where there is no constraint it seems on multiple devices causing it to have a space on larger displays, and exit the screen on small displays within the simulator.
Attempted coding efforts to round the desired corners:
#IBOutlet fileprivate weak var button: UIButton! { didSet {
button.round(corners: [.topRight, .bottomLeft], radius: 50 , borderColor: UIColor.blue, borderWidth: 5.0)
button.setNeedsUpdateConstraints()
button.setNeedsLayout()
} }
override func viewDidLayoutSubviews() {
button.layoutIfNeeded()
}
ovverride func updateViewContraints() {
button.updateConstraintsIfNeeded()
super.updateViewConstraints()
}
The UIView extension that is being used can be found here:
https://stackoverflow.com/a/35621736/5434541
What solution is available to properly adjust the buttons corner radius' and allow the constraints to update the button to as it should be.
Without seeing your full code, I suspect you're getting this because it's drawing multiple layers with the border, and never removing any! If the buttons get resized, the old layers are still there. Here's a snip that shows removing the old layer on redraw that you should be able to adapt. I tested this inside a stack view, and it also behaves correctly on rotation:
class RoundedCorner: UIButton {
var lastBorderLayer:CAShapeLayer?
override func layoutSubviews() { setup() }
func setup() {
let r = self.bounds.size.height / 2
let path = UIBezierPath(roundedRect: self.bounds,
byRoundingCorners: [.topLeft, .bottomRight],
cornerRadii: CGSize(width: r, height: r))
let mask = CAShapeLayer()
mask.path = path.cgPath
addBorder(mask: mask, borderColor: UIColor.blue, borderWidth: 5)
self.layer.mask = mask
}
func addBorder(mask: CAShapeLayer, borderColor: UIColor, borderWidth: CGFloat) {
let borderLayer = CAShapeLayer()
borderLayer.path = mask.path
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = borderColor.cgColor
borderLayer.lineWidth = borderWidth
borderLayer.frame = bounds
if let last = self.lastBorderLayer, let index = layer.sublayers?.index(of: last) {
layer.sublayers?.remove(at: index)
}
layer.addSublayer(borderLayer)
self.lastBorderLayer = borderLayer
}
}
You also noted in your comment about the app lag. I'm not surprised, since layout can get called many times per update, and you're creating a new layer every single time. You can avoid this by saving the last dimensions of the botton frame and comparing. If it's identical, don't recreate the layer.