Drawing a simple line chart using Swift - swift

I am writing a Swift program using Xcode 9.2 for the MacOS with the objecting of displaying a simple line chart with three points.
The program's objective is to create a line graph using apple's core graphics. It is a horizontal line graph with three points, one at the center and at each end. If the mouse pointer is over the center point I want some text to display like "xyz" and when the mouse moves away the text would disappear. The end points will have labels containing numbers but for now let's call them "min" and "max".
Using XCode's playground, so far I have managed to accomplish to display a very clunky line graph using the following code:
class MyView : NSView {
override func draw(_ rect: NSRect) {
NSColor.green.setFill()
var path = NSBezierPath(rect: self.bounds)
path.fill()
let leftDot = NSRect(x: 0, y: 0, width: 10, height: 20)
path = NSBezierPath(ovalIn: leftDot)
NSColor.black.setFill()
path.fill()
let rightDot = NSRect(x: 90, y: 0, width: 10, height: 20)
path = NSBezierPath(ovalIn: rightDot)
NSColor.black.setFill()
path.fill()
let centerDot = NSRect(x: 50, y: 0, width: 10, height: 20)
path = NSBezierPath(ovalIn: centerDot)
NSColor.black.setFill()
path.fill()
}
}
let viewRect = NSRect(x: 0, y: 0, width: 100, height: 10)
let myEmptyView = MyView(frame: viewRect)
The code produces a clunky looking line graph as follows:
The chart below is from Excel and this line chart is much cleaner and is what I am looking for but I want the line to be horizontal instead of diagonal.
Thanks in advance for your help.

To draw a horizontal line similar to the sample image you provided, I took your sample code and modified it. This would give this result:
The code would look like this then:
override func draw(_ rect: NSRect) {
NSColor(red: 103/255, green: 146/255, blue: 195/255, alpha: 1).set()
let line = NSBezierPath()
line.move(to: NSMakePoint(10, 5))
line.line(to: NSMakePoint(90, 5))
line.lineWidth = 2
line.stroke()
NSColor(red: 163/255, green: 189/255, blue: 218/255, alpha: 1).setFill()
let origins = [NSPoint(x: 10, y: 1),
NSPoint(x: 50, y: 1),
NSPoint(x: 90, y: 1)]
let size = NSSize(width: 8, height: 8)
for origin in origins {
let quad = NSBezierPath(roundedRect: NSRect(origin: origin, size: size), xRadius: 2, yRadius: 2)
quad.fill()
quad.stroke()
}
}
Update for iOS
Due to a request in the comments section here the iOS variant:
override func draw(_ rect: CGRect) {
UIColor(red: 103/255, green: 146/255, blue: 195/255, alpha: 1).set()
let line = UIBezierPath()
line.move(to: CGPoint(x: 10, y:5))
line.addLine(to: CGPoint(x: 90, y:5))
line.lineWidth = 2
line.stroke()
UIColor(red: 163/255, green: 189/255, blue: 218/255, alpha: 1).setFill()
let origins = [CGPoint(x: 10, y: 1),
CGPoint(x: 50, y: 1),
CGPoint(x: 90, y: 1)]
let size = CGSize(width: 8, height: 8)
for origin in origins {
let quad = UIBezierPath.init(roundedRect: CGRect(origin: origin, size: size), cornerRadius: 2)
quad.fill()
quad.stroke()
}
}
Alternatively, you could add a CAShapeLayer sublayer whose path property is a UIBezierPath to the UIView.

Related

Drawing a dotted background in swift

Im trying to achieve a dotted view that I can use as a background. Something looking a bit like this:
But I seem to be missing something in my code. I have tried to lab around with different sizes of the dots and everything, but so far I'm just getting my background color set to the view but no dots no matter the size, the color or the spacing. What am I missing?
class DottedBackgroundView: UIView {
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
UIColor.green.setFill()
context.fill(rect)
let drawPattern: CGPatternDrawPatternCallback = { _, context in
context.addArc(
center: CGPoint(x: 5, y: 5), radius: 2.5,
startAngle: 0, endAngle: CGFloat(2.0 * .pi),
clockwise: false)
context.setFillColor(UIColor.white.cgColor)
context.fillPath()
}
var callbacks = CGPatternCallbacks(
version: 0, drawPattern: drawPattern, releaseInfo: nil)
let pattern = CGPattern(
info: nil,
bounds: CGRect(x: 0, y: 0, width: 10, height: 10),
matrix: .identity,
xStep: 10,
yStep: 10,
tiling: .constantSpacing,
isColored: true,
callbacks: &callbacks)
let patternSpace = CGColorSpace(patternBaseSpace: nil)!
context.setFillColorSpace(patternSpace)
var alpha: CGFloat = 1.0
context.setFillPattern(pattern!, colorComponents: &alpha)
context.fill(rect)
context.addArc(
center: CGPoint(x: 5, y: 5), radius: 5.0,
startAngle: 0, endAngle: CGFloat(2.0 * .pi),
clockwise: false)
}
}
You have a bug. Just change second line to UIColor.white.setFill() and inside drawPattern change color to black: context.setFillColor(UIColor.black.cgColor).
It works. I set this view on Storyboard, but if you add it from code try this:
let dottedView = DottedBackgroundView()
dottedView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(dottedView)
NSLayoutConstraint.activate([
dottedView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
dottedView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
dottedView.topAnchor.constraint(equalTo: self.view.topAnchor),
dottedView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
You need to set constraints, because your custom view doesn't have defined size, therefore it's not able to resize properly.

Applying a transformation to UIBezierPath makes drawing disappear

maybe this is a stupid question, but I'm trying to apply a transformation to a path as simple as this:
let path = UIBezierPath(rect: CGRect(x: 10, y: 10, width: 20, height: 20))
path.apply(CGAffineTransform(rotationAngle: 2.0))
"colorLiteral".setFill()
path.fill()
And seems like the fact of adding the "apply" line, completely erases the shape on the view, whereas if I don't include that line, the shape appears on its place correctly.
I'm quite new at Swift so I guess I'm missing an important step on this?
Thanks all!
Try this and remember rotationAngle takes Radians.
let bounds = CGRect(x: 10, y: 10, width: 20, height: 20)
let path = UIBezierPath(rect: bounds)
path.apply(CGAffineTransform(translationX: -bounds.midX, y: -bounds.midY))
path.apply(CGAffineTransform(rotationAngle: CGFloat.pi / 4))
path.apply(CGAffineTransform(translationX: bounds.midX, y: bounds.midY))
Using CATransform3D
shapeLayer.transform = CATransform3DMakeScale(1, -1, 1)
Transforming path,
let shapeBounds = shapeLayer.bounds
let mirror = CGAffineTransform(scaleX: 1,
y: -1)
let translate = CGAffineTransform(translationX: 0,
y: shapeBounds.size.height)
let concatenated = mirror.concatenating(translate)
bezierPath.apply(concatenated)
shapeLayer.path = bezierPath.cgPath
Transforming layer,
let shapeFrame = CGRect(x: -120, y: 120, width: 350, height: 350)
let mirrorUpsideDown = CGAffineTransform(scaleX: 1,
y: -1)
shapeLayer.setAffineTransform(mirrorUpsideDown)
shapeLayer.frame = shapeFrame

How do I animate two views simultaneously in swift programmatically?

In another question, how to animate was answered:
Draw line animated
I then tried to add a view to animate simultaneously. I want to create two animated paths that meet at the same CGPoint but have different paths to get there. I want both paths to start at the same time and end animation at the same time. My current approach is to create a second UIBezierPath called path2, along with a second CAShapeLayer called shapeLayer2, but the second path is just overriding the first. I'm pretty sure both paths need different CAShapeLayers because each path will have different attributes. It seems like I then need to add the CAShapeLayer instance as a view.layer sublayer but it doesn't seem to be working. What should I be doing differently to achieve the desired result?
Here is what ultimately worked.
func animatePath() {
// create whatever path you want
let path = UIBezierPath()
path.move(to: CGPoint(x: 10, y: 100))
path.addLine(to: CGPoint(x: 200, y: 50))
path.addLine(to: CGPoint(x: 200, y: 240))
// create whatever path you want
let path2 = UIBezierPath()
let bottomStartX = view.frame.maxX - 10
let bottomStartY = view.frame.maxY - 100
path2.move(to: CGPoint(x: bottomStartX, y: bottomStartY))
path2.addLine(to: CGPoint(x: bottomStartX - 100, y: bottomStartY - 100))
path2.addLine(to: CGPoint(x: 200, y: 240))
// create shape layer for that path
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
shapeLayer.strokeColor = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1).cgColor
shapeLayer.lineWidth = 4
shapeLayer.path = path.cgPath
let shapeLayer2 = CAShapeLayer()
shapeLayer2.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
shapeLayer2.strokeColor = UIColor.blue.cgColor
shapeLayer2.lineWidth = 4
shapeLayer2.path = path.cgPath
// animate it
view.layer.addSublayer(shapeLayer)
view.layer.addSublayer(shapeLayer2)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 2
shapeLayer.add(animation, forKey: "MyAnimation")
let animation2 = CABasicAnimation(keyPath: "strokeEnd")
animation2.fromValue = 0
animation2.duration = 2
shapeLayer2.add(animation2, forKey: "MyAnimation")
// save shape layer
self.shapeLayer = shapeLayer
}

Add a border/shadow effect to UIView

I've managed to draw a speech bubble with following code:
override func draw(_ rect: CGRect) {
let rounding:CGFloat = rect.width * 0.01
//Draw the main frame
let bubbleFrame = CGRect(x: 0, y: 0, width: rect.width, height: rect.height * 6 / 7)
let bubblePath = UIBezierPath(roundedRect: bubbleFrame,
byRoundingCorners: UIRectCorner.allCorners,
cornerRadii: CGSize(width: rounding, height: rounding))
//Color the bubbleFrame
color.setStroke()
color.setFill()
bubblePath.stroke()
bubblePath.fill()
//Add the point
let context = UIGraphicsGetCurrentContext()
//Start the line
context!.beginPath()
context?.move(to: CGPoint(x: bubbleFrame.minX + bubbleFrame.width * 1/2 - 8, y: bubbleFrame.maxY))
//Draw a rounded point
context?.addArc(tangent1End: CGPoint(x: rect.maxX * 1/2 + 4, y: rect.maxY), tangent2End: CGPoint(x:bubbleFrame.maxX , y: bubbleFrame.minY), radius: 0)
//Close the line
context?.addLine(to: CGPoint(x: bubbleFrame.minX + bubbleFrame.width * 1/2 + 16, y: bubbleFrame.maxY))
context!.closePath()
//fill the color
context?.setFillColor(UIColor.white.cgColor)
context?.fillPath()
context?.saveGState()
context?.setShadow(offset:CGSize(width: 1, height: 1), blur: 2, color: UIColor.gray.cgColor)
UIColor.gray.setStroke()
bubblePath.lineWidth = 0.5
bubblePath.stroke()
context?.restoreGState()
}
At the moment, it looks like this:
I seem to be having trouble adding a shadow effect to the speech bubble. I want the point at the bottom to have the shadow effect, no just the rectangle above. Also, the shadow/border at the top is too thick. This is the end result I'm looking to achieve:

UIBezierPath Rounded corners

I am trying to make this image appear with some rounded corners, dictated in code below
My code for this is as follows. I want path1,path[4], path[5] rounded. I am open to a different approach. Please dont recommend UIBezierPath(roundedRect:..) as I dont need corenrs 0,2 rounded. This is tricky!
let width = self.view.frame.size.width
let trianglePath = UIBezierPath()
var pts = [CGPoint(x: 2 * width / 16, y: width / 16),
CGPoint(x: width / 32, y: width / 16 + width / 16),
CGPoint(x: 2 * width / 16 , y: width / 16 + width / 8),
CGPoint(x: width / 5 + 2 * width / 16, y: width / 8 + width / 16),
CGPoint(x: width / 5 + 2 * width / 16, y: width / 16 ),
CGPoint(x: 2 * width / 16, y: width / 16 )
]
// this path replaces this, because i cannot easily add rectangle
//var path = UIBezierPath(roundedRect: rect, cornerRadius: width / 37.5).cgPath
trianglePath.move(to: pts[0])
trianglePath.addLine(to: pts[1]) // needs to be rounded
trianglePath.addLine(to: pts[2])
trianglePath.addLine(to: pts[3])
trianglePath.addLine(to: pts[4]) // needs to be rounded
trianglePath.addLine(to: pts[5]) // needs to be rounded
trianglePath.close()
let layer = CAShapeLayer()
layer.path = trianglePath.cgPath
layer.fillColor = UIColor.blue.cgColor
layer.lineCap = kCALineCapRound
layer.lineJoin = kCALineJoinRound
layer.zPosition = 4
layer.isHidden = false
view.layer.addSublayer(layer)
You can create a clipping path as explained here: https://www.raywenderlich.com/162313/core-graphics-tutorial-part-2-gradients-contexts
Specifically this bit:
let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: 8.0, height: 8.0))
path.addClip()
If you want to make rounded shapes you must use
trianglePath.AddArcToPoint(...);
See documentation here: https://developer.apple.com/library/content/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/BezierPaths/BezierPaths.html#//apple_ref/doc/uid/TP40010156-CH11-SW5
To be extra helpful, here is an entire subclass which you can use to learn how to draw custom views. Hope this helps.
import Foundation
import UIKit
final class BorderedView:UIView
{
var drawTopBorder = false
var drawBottomBorder = false
var drawLeftBorder = false
var drawRightBorder = false
var topBorderColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.4)
var leftBorderColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.4)
var rightBorderColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
var bottomBorderColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
var upperLeftCornerRadius:CGFloat = 0
var upperRightCornerRadius:CGFloat = 0
var lowerLeftCornerRadius:CGFloat = 0
var lowerRightCornerRadius:CGFloat = 0
var subview:UIView?
fileprivate var visibleView:UIView!
fileprivate var _backgroundColor:UIColor!
override init(frame: CGRect)
{
super.init(frame: frame)
super.backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
super.backgroundColor = UIColor.clear
}
func setVisibleBackgroundColor(_ color:UIColor)
{
_backgroundColor = color
}
func setBackgroundGradient(_ gradient:CAGradientLayer)
{
gradient.frame = visibleView.bounds
gradient.masksToBounds = true
visibleView.layer.insertSublayer(gradient, at: 0)
}
func drawView()
{
visibleView = UIView(frame: self.frame)
visibleView.backgroundColor = _backgroundColor
visibleView.clipsToBounds = true
if let v = subview
{
visibleView.addSubview(v)
}
}
override func draw(_ rect: CGRect)
{
// Drawing code
let width = rect.size.width - 0.5;
let height = rect.size.height - 0.5;
guard let context = UIGraphicsGetCurrentContext() else { return }
let w = self.frame.size.width;
let h = self.frame.size.height;
// Create clipping area as path.
let path = CGMutablePath();
path.move(to: CGPoint(x: 0, y: upperLeftCornerRadius))
path.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: upperLeftCornerRadius, y: 0), radius: upperLeftCornerRadius)
path.addLine(to: CGPoint(x: w - upperRightCornerRadius, y: 0))
path.addArc(tangent1End: CGPoint(x: w, y: 0), tangent2End: CGPoint(x: w, y: upperRightCornerRadius), radius: upperRightCornerRadius)
path.addLine(to: CGPoint(x: w, y: h - lowerRightCornerRadius))
path.addArc(tangent1End: CGPoint(x: w, y: h), tangent2End: CGPoint(x: w - lowerRightCornerRadius, y: h), radius: lowerRightCornerRadius)
path.addLine(to: CGPoint(x: lowerLeftCornerRadius, y: h))
path.addArc(tangent1End: CGPoint(x: 0, y: h), tangent2End: CGPoint(x: 0, y: h - lowerLeftCornerRadius), radius: lowerLeftCornerRadius)
path.closeSubpath();
// Add clipping area to path
context.addPath(path);
// Clip to the path and draw the image
context.clip ();
self.drawView()
visibleView.layer.render(in: context)
// ********** Your drawing code here **********
context.setLineWidth(0.5);
var stroke = false
if (drawTopBorder)
{
context.setStrokeColor(topBorderColor.cgColor);
context.move(to: CGPoint(x: 0, y: 0.5)); //start at this point
context.addLine(to: CGPoint(x: width, y: 0.5)); //draw to this point
stroke = true
}
if (drawLeftBorder)
{
context.setStrokeColor(leftBorderColor.cgColor);
context.move(to: CGPoint(x: 0.5, y: 0.5)); //start at this point
context.addLine(to: CGPoint(x: 0.5, y: height)); //draw to this point
stroke = true
}
if (drawBottomBorder)
{
context.setStrokeColor(bottomBorderColor.cgColor);
context.move(to: CGPoint(x: 0.5, y: height)); //start at this point
context.addLine(to: CGPoint(x: width, y: height)); //draw to this point
stroke = true
}
if (drawRightBorder)
{
context.setStrokeColor(rightBorderColor.cgColor);
context.move(to: CGPoint(x: width, y: 0.5)); //start at this point
context.addLine(to: CGPoint(x: width, y: height)); //draw to this point
stroke = true
}
// and now draw the Path!
if stroke
{
context.strokePath();
}
}
}