How to draw dashed arrow? - ios

I want to draw arrow like this:
I found how to draw just solid arrow here, but i don't know how to draw arrow like above.
Solution:
For me I ended up with code below:
func addArrowOntoView(view: UIView, startPoint: CGPoint, endPoint: CGPoint, color: UIColor) {
let line = UIBezierPath()
line.moveToPoint(startPoint)
line.addLineToPoint(endPoint)
let arrow = UIBezierPath()
arrow.moveToPoint(endPoint)
arrow.addLineToPoint(CGPointMake(endPoint.x - 5, endPoint.y - 4))
arrow.moveToPoint(endPoint)
arrow.addLineToPoint(CGPointMake(endPoint.x - 5, endPoint.y + 4))
arrow.lineCapStyle = .Square
let sublayer = CAShapeLayer()
sublayer.path = line.CGPath
view.layer.addSublayer(sublayer)
//add Line
let lineLayer = CAShapeLayer()
lineLayer.path = line.CGPath
lineLayer.strokeColor = color.CGColor
lineLayer.lineWidth = 1.0
lineLayer.lineDashPattern = [5, 3]
view.layer.addSublayer(lineLayer)
//add Arrow
let arrowLayer = CAShapeLayer()
arrowLayer.path = arrow.CGPath
arrowLayer.strokeColor = color.CGColor
arrowLayer.lineWidth = 1.0
view.layer.addSublayer(arrowLayer)
}

Here is a code for such an ArrowView that I wrote to get this in a playground:
//ArrowView
class ArrowView : UIView {
var dashWidth :CGFloat = 3.0
var dashGap : CGFloat = 3.0
var arrowThickNess : CGFloat = 2.0
var arrowLocationX : CGFloat = 0.0
//MARK:
override func drawRect(rect: CGRect) {
//Compute the dashPath
let path = UIBezierPath()
//Compute the mid y, path height
let midY = CGRectGetMidY(frame)
let pathHeight = CGRectGetHeight(frame)
path.moveToPoint(CGPointMake(frame.origin.x, midY))
path.addLineToPoint(CGPointMake(frame.origin.x + frame.size.width - dashWidth , midY))
path.lineWidth = arrowThickNess
let dashes: [CGFloat] = [dashWidth, dashGap]
path.setLineDash(dashes, count: dashes.count, phase: 0)
//Arrow
let arrow = UIBezierPath()
arrow.lineWidth = arrowThickNess
arrow.moveToPoint(CGPointMake(frame.origin.x + arrowLocationX , midY))
arrow.addLineToPoint(CGPointMake(frame.origin.x + frame.size.width - arrowThickNess/2 - 18, 0))
arrow.moveToPoint(CGPointMake(frame.origin.x + arrowLocationX , midY))
arrow.addLineToPoint(CGPointMake(frame.origin.x + frame.size.width - arrowThickNess/2 - 18 , pathHeight))
arrow.lineCapStyle = .Square
UIColor.whiteColor().set()
path.stroke()
arrow.stroke()
}
}
let arrowView = ArrowView(frame: CGRect(x: 0, y: 0, width: 210, height: 20))
arrowView.dashGap = 10
arrowView.dashWidth = 5
arrowView.arrowLocationX = 202
arrowView.setNeedsDisplay()
Basically you will need to create a bezier path with required line dashes and you will need to supply the dashes as an array of float values. At the end of this bezier path, you will need to draw another bezier path representing the arrow.
Output:-

Related

How to create Circle with step progress (gaps in it) and animate it?

I need to create a progressive with gaps in it and Animate the layers. I have achieved it. But the problem is it is starting (0) from Right centre. But the requirement is it should start from top centre. In image You can see that it is started from right side.
I have attached my code sample along with Image for your understanding. Can somebody help me where I'm doing wrong or how should I make it from top.
extension ViewController {
func sampleProgress() {
let totalSteps = 6
let frame = CGRect(x: 50, y: 50, width: 120, height: 120)
let circlePath = UIBezierPath(ovalIn: frame)
let gapSize: CGFloat = 0.0125
let segmentAngle: CGFloat = 0.167 // (1/totalSteps)
var startAngle = 0.0
let lineWidth = 8.0
for index in 0 ... totalSteps {
// Background layer
let backgroundLayer = CAShapeLayer()
backgroundLayer.strokeStart = startAngle
backgroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
backgroundLayer.path = circlePath.cgPath
backgroundLayer.name = String(index)
backgroundLayer.strokeColor = UIColor.lightGray.cgColor
backgroundLayer.lineWidth = lineWidth
backgroundLayer.lineCap = CAShapeLayerLineCap.butt
backgroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(backgroundLayer)
// Foreground layer
let foregroundLayer = CAShapeLayer()
foregroundLayer.strokeStart = startAngle
foregroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
foregroundLayer.isHidden = true
foregroundLayer.name = String(index) + String(index)
foregroundLayer.path = circlePath.cgPath
foregroundLayer.strokeColor = UIColor.green.cgColor
foregroundLayer.lineWidth = lineWidth
foregroundLayer.lineCap = CAShapeLayerLineCap.butt
foregroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(foregroundLayer)
print("Start angle: \(startAngle)")
startAngle = startAngle + segmentAngle
}
}
func animateLayer(isAnimate: Bool, stepsToAnimate: Int) {
let segmentAngle: CGFloat = (360 * 0.166) / 360
let gapSize: CGFloat = 0.0125
var startAngle = 0.0
for index in 0 ... stepsToAnimate {
if let foregroundLayers = self.view.layer.sublayers {
for animateLayer in foregroundLayers {
if animateLayer.name == String(index) + String(index) {
if index == stepsToAnimate && isAnimate {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = startAngle
animation.toValue = startAngle + segmentAngle - gapSize
animation.duration = 1.0
animateLayer.add(animation, forKey: "foregroundAnimation")
animateLayer.isHidden = false
} else {
animateLayer.isHidden = false
}
startAngle = startAngle + segmentAngle
}
}
}
}
}
}
You can "move the start" to the top by rotating the layer(s) minus 90-degrees:
let tr = CATransform3DMakeRotation(-(.pi * 0.5), 0, 0, 1)
I would assume this would be wrapped into a UIView subclass, but to get your example (adding sublayers to the main view's layer) to work right, we'll want to use a Zero-based origin for the path rect:
// use 0,0 for the origin of the PATH frame
let frame = CGRect(x: 0, y: 0, width: 120, height: 120)
let circlePath = UIBezierPath(ovalIn: frame)
and then an offset rect for the position:
let layerFrame = frame.offsetBy(dx: 50, dy: 50)
and we set the .anchorPoint of the layers to the center of that rect -- so it will rotate around its center:
// set the layer's frame
backgroundLayer.frame = layerFrame
// set the layer's anchor point
backgroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
backgroundLayer.transform = tr
// set the layer's frame
foregroundLayer.frame = layerFrame
// set the layer's anchor point
foregroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
foregroundLayer.transform = tr
So, slight modifications to your code:
extension ViewController {
func sampleProgress() {
let totalSteps = 6
// use 0,0 for the origin of the PATH frame
let frame = CGRect(x: 0, y: 0, width: 120, height: 120)
let circlePath = UIBezierPath(ovalIn: frame)
// use this for the POSITION of the path
let layerFrame = frame.offsetBy(dx: 50, dy: 50)
let gapSize: CGFloat = 0.0125
let segmentAngle: CGFloat = 0.167 // (1/totalSteps)
var startAngle = 0.0
let lineWidth = 8.0
// we want to rotate the layer by -90 degrees
let tr = CATransform3DMakeRotation(-(.pi * 0.5), 0, 0, 1)
for index in 0 ... totalSteps {
// Background layer
let backgroundLayer = CAShapeLayer()
backgroundLayer.strokeStart = startAngle
backgroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
backgroundLayer.path = circlePath.cgPath
backgroundLayer.name = String(index)
backgroundLayer.strokeColor = UIColor.lightGray.cgColor
backgroundLayer.lineWidth = lineWidth
backgroundLayer.lineCap = CAShapeLayerLineCap.butt
backgroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(backgroundLayer)
// set the layer's frame
backgroundLayer.frame = layerFrame
// set the layer's anchor point
backgroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
backgroundLayer.transform = tr
// Foreground layer
let foregroundLayer = CAShapeLayer()
foregroundLayer.strokeStart = startAngle
foregroundLayer.strokeEnd = backgroundLayer.strokeStart + segmentAngle - gapSize
foregroundLayer.isHidden = true
foregroundLayer.name = String(index) + String(index)
foregroundLayer.path = circlePath.cgPath
foregroundLayer.strokeColor = UIColor.green.cgColor
foregroundLayer.lineWidth = lineWidth
foregroundLayer.lineCap = CAShapeLayerLineCap.butt
foregroundLayer.fillColor = UIColor.clear.cgColor
self.view.layer.addSublayer(foregroundLayer)
// set the layer's frame
foregroundLayer.frame = layerFrame
// set the layer's anchor point
foregroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
foregroundLayer.transform = tr
print("Start angle: \(startAngle)")
startAngle = startAngle + segmentAngle
}
}
func animateLayer(isAnimate: Bool, stepsToAnimate: Int) {
let segmentAngle: CGFloat = (360 * 0.166) / 360
let gapSize: CGFloat = 0.0125
var startAngle = 0.0
for index in 0 ... stepsToAnimate {
if let foregroundLayers = self.view.layer.sublayers {
for animateLayer in foregroundLayers {
if animateLayer.name == String(index) + String(index) {
if index == stepsToAnimate && isAnimate {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = startAngle
animation.toValue = startAngle + segmentAngle - gapSize
animation.duration = 1.0
animateLayer.add(animation, forKey: "foregroundAnimation")
animateLayer.isHidden = false
} else {
animateLayer.isHidden = false
}
startAngle = startAngle + segmentAngle
}
}
}
}
}
}
and an example controller - each tap anywhere animates the next step:
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
sampleProgress()
}
var p: Int = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
animateLayer(isAnimate: true, stepsToAnimate: p)
p += 1
}
}

iOS Radar Chart with 3D Effect

I would like to replicate this graph in my app.
I tried to search online but I found only pods, specifically Charts.
I tried to customize it but I was unable to give it the 3d effect by assigning each "triangle" a different color shade.
How can I replicate it?
Uibezierpath or something else?
Just have fun.
class GraphView: UIView {
let cirleSegnaposto:CGFloat = 20.0
let labelSize:Double = 50
let spacingGraphLabel:Double = 0
let widthOfZero:Double = 30
let labels = ["Label 1", "Label 2", "Label 3", "Label 4", "Label 5"]
let firstColors:[UIColor] = [.darkGray, .black, .darkGray, .lightGray, .white]
let secondColors:[UIColor] = [.orange, .brown, .orange, .yellow, .red]
var values: [Int]? = nil
var secondValues: [Int]? = nil
override func draw(_ rect: CGRect) {
for i in 0 ..< 4 {
let cirleLayer = CAShapeLayer()
let delta = Double(15 * i) + labelSize
let path = UIBezierPath(ovalIn: CGRect(x: delta,
y: delta,
width: Double(rect.width) - delta * 2,
height: Double(rect.width) - delta * 2))
cirleLayer.path = path.cgPath
cirleLayer.lineWidth = 1
cirleLayer.strokeColor = UIColor.lightGray.cgColor
cirleLayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(cirleLayer)
}
let radius:Double = Double(rect.width/2) - (labelSize - spacingGraphLabel)
let labelRadius:Double = Double(rect.width/2) + (spacingGraphLabel)
let origin = CGPoint(x: rect.width/2, y: rect.height/2)
for i in 0..<5 {
let cirleLayer = CAShapeLayer()
let angle:Double = Double(i)/5.0 * (2 * .pi)
let centerX = Double(origin.x) + radius * cos(angle)
let centerY = Double(origin.y) - radius * sin(angle)
let path = UIBezierPath(ovalIn: CGRect(x: CGFloat(centerX) - cirleSegnaposto/2,
y: CGFloat(centerY) - cirleSegnaposto/2,
width: cirleSegnaposto,
height: cirleSegnaposto))
cirleLayer.path = path.cgPath
cirleLayer.fillColor = UIColor.lightGray.cgColor
cirleLayer.lineWidth = 0.5
cirleLayer.strokeColor = UIColor.black.cgColor
self.layer.addSublayer(cirleLayer)
let label = UILabel(frame: .zero)
label.font = UIFont.systemFont(ofSize: 12)
label.text = labels[i]
label.frame.size = CGSize(width: labelSize, height: labelSize/2)
let labelCenterX = Double(origin.x) + labelRadius * cos(angle)
let labelCenterY = Double(origin.y) - labelRadius * sin(angle)
label.center = CGPoint(x: labelCenterX, y: labelCenterY)
label.transform = label.transform.rotated(by: .pi/2)
self.addSubview(label)
}
if let values = secondValues {
drawGraph(values: values, center: origin, maxValue: radius, colors: secondColors.map({$0.cgColor}))
}
if let values = values {
drawGraph(values: values, center: origin, maxValue: radius, colors: firstColors.map({$0.cgColor}))
}
}
func drawGraph(values: [Int], center: CGPoint, maxValue: Double, colors: [CGColor]) {
var points = [CGPoint]()
for i in 0 ..< values.count {
let radius = Double(values[i])/10.0 * (maxValue - widthOfZero) + widthOfZero
let angle:Double = Double(i)/5.0 * (2 * .pi)
let x = Double(center.x) + radius * cos(angle)
let y = Double(center.y) - radius * sin(angle)
let point = CGPoint(x: x, y: y)
points.append(point)
}
for (i, point) in points.enumerated() {
let secondPoint = point == points.last ? points[0] : points[i+1]
let path = UIBezierPath()
path.move(to: center)
path.addLine(to: point)
path.addLine(to: secondPoint)
path.close()
let layer = CAShapeLayer()
layer.path = path.cgPath
layer.fillColor = colors[i]
layer.lineWidth = 1
layer.lineJoin = .round
layer.strokeColor = UIColor.black.cgColor
self.layer.addSublayer(layer)
}
}
}

Dashed horizontal line using IBDesignable

So i came across this question, and i want to achieve this to draw a horizontal line with the same approach using #IBDesignable.
I have tried to play around inside the class, but no result.
#IBDesignable class DottedVertical: UIView {
#IBInspectable var dotColor: UIColor = UIColor.etc
#IBInspectable var lowerHalfOnly: Bool = false
override func draw(_ rect: CGRect) {
// say you want 8 dots, with perfect fenceposting:
let totalCount = 8 + 8 - 1
let fullHeight = bounds.size.height
let width = bounds.size.width
let itemLength = fullHeight / CGFloat(totalCount)
let path = UIBezierPath()
let beginFromTop = CGFloat(0.0)
let top = CGPoint(x: width/2, y: beginFromTop)
let bottom = CGPoint(x: width/2, y: fullHeight)
path.move(to: top)
path.addLine(to: bottom)
path.lineWidth = width
let dashes: [CGFloat] = [itemLength, itemLength]
path.setLineDash(dashes, count: dashes.count, phase: 0)
// for ROUNDED dots, simply change to....
//let dashes: [CGFloat] = [0.0, itemLength * 2.0]
//path.lineCapStyle = CGLineCap.round
dotColor.setStroke()
path.stroke()
}
}
You can achieve as below,
#IBDesignable class DottedHorizontal: UIView {
#IBInspectable var dotColor: UIColor = UIColor.red
#IBInspectable var lowerHalfOnly: Bool = false
override func draw(_ rect: CGRect) {
let fullHeight = bounds.size.height
let width = bounds.size.width
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: fullHeight/2))
path.addLine(to: CGPoint(x: width, y: fullHeight/2))
path.lineWidth = 5
let dashes: [CGFloat] = [4, 2]
path.setLineDash(dashes, count: dashes.count, phase: 0)
dotColor.setStroke()
path.stroke()
}
}

shadow not visible on view with custom shape

on my imageview shadow is not visible after i changed the shape of it to hexagon here's how i'm changing imageView's shape :
extension UIView {
func makeHexagon(){
let lineWidth: CGFloat = 3
let path = UIBezierPath(roundedPolygonPathWithRect: self.bounds, lineWidth: lineWidth, sides: 6, cornerRadius: 1)
let mask = CAShapeLayer()
mask.path = path.cgPath
mask.lineWidth = lineWidth
mask.strokeColor = UIColor.clear.cgColor //controls the stroke width
mask.fillColor = UIColor.white.cgColor
self.layer.mask = mask
let border = CAShapeLayer()
border.path = path.cgPath
border.lineWidth = lineWidth
border.strokeColor = UIColor.black.cgColor
border.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(border)
}
}
............................
extension UIBezierPath {
convenience init(roundedPolygonPathWithRect rect: CGRect, lineWidth: CGFloat, sides: NSInteger, cornerRadius: CGFloat) {
self.init()
let theta = CGFloat(2.0 * M_PI) / CGFloat(sides)
let offSet = CGFloat(cornerRadius) / CGFloat(tan(theta/2.0))
let squareWidth = min(rect.size.width, rect.size.height)
var length = squareWidth - lineWidth
if sides%4 != 0 {
length = length * CGFloat(cos(theta / 2.0)) + offSet/2.0
}
let sideLength = length * CGFloat(tan(theta / 2.0))
var point = CGPoint(x: squareWidth / 2.0 + sideLength / 2.0 - offSet, y: squareWidth - (squareWidth - length) / 2.0)
var angle = CGFloat(M_PI)
move(to: point)
for _ in 0 ..< sides {
point = CGPoint(x: point.x + CGFloat(sideLength - offSet * 2.0) * CGFloat(cos(angle)), y: point.y + CGFloat(sideLength - offSet * 2.0) * CGFloat(sin(angle)))
addLine(to: point)
let center = CGPoint(x: point.x + cornerRadius * CGFloat(cos(angle + CGFloat(M_PI_2))), y: point.y + cornerRadius * CGFloat(sin(angle + CGFloat(M_PI_2))))
addArc(withCenter: center, radius:CGFloat(cornerRadius), startAngle:angle - CGFloat(M_PI_2), endAngle:angle + theta - CGFloat(M_PI_2), clockwise:true)
point = currentPoint // we don't have to calculate where the arc ended ... UIBezierPath did that for us
angle += theta
}
close()
}
}
using extension :
firstImageView.makeHexagon()
and here's how i'm adding my shadow Effect on my ImageView :
firstImageView.layer.contentsScale = UIScreen.main.scale;
firstImageView.layer.shadowColor = UIColor.black.cgColor;
firstImageView.layer.shadowOffset = CGSize.zero;
firstImageView.layer.shadowRadius = 5.0;
firstImageView.layer.shadowOpacity = 2;
firstImageView.layer.masksToBounds = false;
firstImageView.clipsToBounds = false;
anyone can point out my shadow is not visible after changing the shape of imageView ???
You have clipped your view to hexagon, so to display shadow you have to set shadow on ShapeLayer.
let border = CAShapeLayer()
border.path = path.cgPath
border.lineWidth = lineWidth
border.strokeColor = UIColor.black.cgColor
border.fillColor = UIColor.clear.cgColor
border.shadowColor = UIColor.red.cgColor;
border.shadowRadius = 5.0;
border.shadowOpacity = 2;

UILabel bottom and right border

I tried to add border for a uilabel, but I only want to have top, right, and bottom border.
Like this:
|
I am a label |
|
----------------
I tried to use these codes, but it adds all 4 sides by default
myLabel.layer.borderWidth = 1;
myLabel.layer.borderColor = UIColorCode.init(hexString: "#666666")
Create a subclass of UILabel and add the following code. This will draw borders as you need.
override func drawRect(rect: CGRect) {
let outerBorder = UIColor.blackColor()
let lineWidth : CGFloat = 2.0
let insetRect = rect.insetBy(dx: lineWidth/2, dy: lineWidth/2)
let startingTopPoint = CGPointMake(insetRect.origin.x,insetRect.origin.y)
let endingTopPoint = CGPoint(x: insetRect.maxX, y: insetRect.minY)
let bottomLeft = CGPoint(x: insetRect.minX, y: insetRect.maxY)
let bottomRight = CGPoint(x: insetRect.maxX, y: insetRect.maxY)
let path = UIBezierPath()
path.moveToPoint(startingTopPoint)
path.addLineToPoint(endingTopPoint)
path.lineWidth = 2.0
path.addLineToPoint(bottomRight)
path.addLineToPoint(bottomLeft)
outerBorder.setStroke()
path.stroke()
}
let borderWidth: CGFloat = 1.0
let borderLayer = CAShapeLayer()
borderLayer.lineWidth = borderWidth
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.strokeColor = UIColor.blueColor().CGColor
let borderLine = UIBezierPath()
borderLine.moveToPoint(CGPoint(x: 0, y: myLabel.bounds.height - borderWidth / 2))
borderLine.addLineToPoint(CGPoint(x: myLabel.bounds.width - borderWidth / 2, y: myLabel.bounds.height - borderWidth / 2))
borderLine.addLineToPoint(CGPoint(x: myLabel.bounds.width - borderWidth / 2, y: 0))
borderLayer.path = borderLine.CGPath
myLabel.layer.addSublayer(borderLayer)

Resources