Drawing a scale on semi-circle Swift/UIView - ios

Imagine I am having a full semi-circle from 0 to Pi from the unit circle. There is a small number on the left side named min and a big number on the right side called max. There are both interchangeable inside the app depending on some factors.
Does anybody of you have a nice idea on how to draw a scale like I did in the drawing below? I would like to have longer lines for every x mod 10 = 0 and three larger ones in between. The grey circle is just for orientation.
So I started with the following piece of code:
let radius = CGFloat(40)
let dashLong = CGFloat(10)
let dashShort = CGFloat(5)
let middle = CGPoint(x: 50, y: 50)
let leftAngle = CGFloat(Double.pi)
let rightAngle = CGFloat(0)
let min = 45 //random num
let max = 117 //random num
let innerPath = UIBezierPath(arcCenter: middle, radius: radius, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
let middlePath = UIBezierPath(arcCenter: middle, radius: radius+dashShort, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
let outerPath = UIBezierPath(arcCenter: middle, radius: radius+dashLong, startAngle: rightAngle, endAngle: leftAngle, clockwise: true)
So there is a radius and also the length of the two types of dashes in the scale. I chose 45 and 117 as random integers for the extrem values of the scale. My three paths which do not need to be drawn are just an orientation on where the dashes need to be started and ended on. So for 50,60,...110 there start at the innerPath and go to the outer one, I am pretty sure that must be in the same angle for a dash on all circles.
Does anyone has a very smart idea how to continue this to calc the dashes and draw them without getting messed up code?

Here's the math for drawing a tick mark.
Let's do everything as CGFloat to keep the conversions to a minimum:
let radius: CGFloat = 40
let dashLong: CGFloat = 10
let dashShort: CGFloat 5
let middle = CGPoint(x: 50, y: 50)
let leftAngle: CGFloat = .pi
let rightAngle: CGFloat = 0
let min: CGFloat = 45 //random num
let max: CGFloat = 117 //random num
First, compute your angle.
let value: CGFloat = 50
let angle = (max - value)/(max - min) * .pi
Now compute your two points:
let p1 = CGPoint(x: middle.x + cos(angle) * radius,
y: middle.y - sin(angle) * radius)
// use dashLong for a long tick, and dashShort for a short tick
let radius2 = radius + dashLong
let p2 = CGPoint(x: middle.x + cos(angle) * radius2,
y: middle.y - sin(angle) * radius2)
Then draw a line between p1 and p2.
Note: In iOS, the coordinate system is upside down with +Y being down, which is why the sin calculation is subtracted from middle.y.
Complete Example
enum TickStyle {
case short
case long
}
class ScaleView: UIView {
// ScaleView properties. If any are changed, redraw the view
var radius: CGFloat = 40 { didSet { self.setNeedsDisplay() } }
var dashLong: CGFloat = 10 { didSet { self.setNeedsDisplay() } }
var dashShort: CGFloat = 5 { didSet { self.setNeedsDisplay() } }
var middle = CGPoint(x: 50, y: 50) { didSet { self.setNeedsDisplay() } }
var leftAngle: CGFloat = .pi { didSet { self.setNeedsDisplay() } }
var rightAngle: CGFloat = 0 { didSet { self.setNeedsDisplay() } }
var min: CGFloat = 45 { didSet { self.setNeedsDisplay() } }
var max: CGFloat = 117 { didSet { self.setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
// draw the arc
path.move(to: CGPoint(x: middle.x - radius, y: middle.y))
path.addArc(withCenter: middle, radius: radius, startAngle: leftAngle, endAngle: rightAngle, clockwise: true)
let startTick = ceil(min / 2.5) * 2.5
let endTick = floor(max / 2.5) * 2.5
// add tick marks every 2.5 units
for value in stride(from: startTick, through: endTick, by: 2.5) {
let style: TickStyle = value.truncatingRemainder(dividingBy: 10) == 0 ? .long : .short
addTick(at: value, style: style, to: path)
}
// stroke the path
UIColor.black.setStroke()
path.stroke()
}
// add a tick mark at value with style to path
func addTick(at value: CGFloat, style: TickStyle, to path: UIBezierPath) {
let angle = (max - value)/(max - min) * .pi
let p1 = CGPoint(x: middle.x + cos(angle) * radius,
y: middle.y - sin(angle) * radius)
var radius2 = radius
if style == .short {
radius2 += dashShort
} else if style == .long {
radius2 += dashLong
}
let p2 = CGPoint(x: middle.x + cos(angle) * radius2,
y: middle.y - sin(angle) * radius2)
path.move(to: p1)
path.addLine(to: p2)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view = ScaleView(frame: CGRect(x: 50, y: 50, width: 100, height: 60))
view.backgroundColor = .yellow
self.view.addSubview(view)
}
}
Picture of scale running in app:

My suggestion is Draw this semi circle in CALayer and Draw lines from centre of the semi circle in different CALayer and Mask both of them so that It appears Like this

Related

Get N number of CGPoints around a circle on UIView

I got a center CGPoint and radius Float, I need to get N number of points surrounding the circle, for example in below image how to get the 12 points corresponding red dots.
This is my incomplete function:
func getCirclePoints(centerPoint point: CGPoint, and radius: CGFloat, n: Int) [CGPoint] {
let result: [CGPoint] = stride(from: 0.0, to: 360.0, by: CGFloat(360 / n)).map {
let bearing = $0 * .pi / 180
// NO IDEA WHERE TO MOVE FURTHER
}
return result
}
getCirclePoints(centerPoint: CGPoint(x: 160, y: 240), radius: 120.0, n: 12)
You were almost there!
func getCirclePoints(centerPoint point: CGPoint, radius: CGFloat, n: Int)->[CGPoint] {
let result: [CGPoint] = stride(from: 0.0, to: 360.0, by: Double(360 / n)).map {
let bearing = CGFloat($0) * .pi / 180
let x = point.x + radius * cos(bearing)
let y = point.y + radius * sin(bearing)
return CGPoint(x: x, y: y)
}
return result
}
let points = getCirclePoints(centerPoint: CGPoint(x: 160, y: 240), radius: 120.0, n: 12)
I didn't think and was very clear as an argument name so I've removed this.
Use radians instead of degrees. They are needed inside trigonometric functions
func getCirclePoints(centerPoint point: CGPoint, and radius: CGFloat, n: Int) -> [CGPoint] {
return Array(repeating: 0, count: n).enumerated().map { offset, element in
let cgFloatIndex = CGFloat(offset)
let radiansStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
let radians = radiansStep * cgFloatIndex
let x = cos(radians) * radius + point.x
let y = sin(radians) * radius + point.y
return CGPoint(x: x, y: y)
}
}
func getCirclePoints1(centerPoint point: CGPoint, and radius: CGFloat, n: Int) -> [CGPoint] {
var resultPoints: [CGPoint] = []
let radianStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
for radians in stride(from: CGFloat(0.0), to: CGFloat.pi * CGFloat(2.0), by: radianStep) {
let x = cos(radians) * radius + point.x
let y = sin(radians) * radius + point.y
resultPoints.append(CGPoint(x: x, y: y))
}
return resultPoints
}
func getCirclePoints2(centerPoint point: CGPoint, and radius: CGFloat, n: Int) -> [CGPoint] {
let radianStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
return stride(from: CGFloat(0.0), to: CGFloat.pi * CGFloat(2.0), by: radianStep).map { element in
let cgFloatIndex = CGFloat(element)
let radiansStep = CGFloat.pi * CGFloat(2.0) / CGFloat(n)
let radians = radiansStep * cgFloatIndex
let x = cos(radians) * radius + point.x
let y = sin(radians) * radius + point.y
return CGPoint(x: x, y: y)
}
}
getCirclePoints(centerPoint: CGPoint(x: 160, y: 240), and: 120.0, n: 12)
For having a draw reference
import UIKit
let numOfItems = 10
class customView : UIView {
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
for i in 0...numOfItems
{
let angle = 360/CGFloat(numOfItems) * CGFloat(i) * .pi / 180
let rad = self.bounds.size.width/2 - 10
let x = bounds.midX + cos(angle) * rad
let y = bounds.midY + sin(angle) * rad
let circlePath = UIBezierPath(
arcCenter: CGPoint(x:x,y:y),
radius:10,
startAngle:0,
endAngle:360,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 3
shapeLayer.path = circlePath.cgPath
layer.addSublayer(shapeLayer)
}
}
}

Transparent semi circle in top center of UIView in swift [duplicate]

I want to crop an UIView with bottom and top of repeated semi circle like this image
I had been working on your question and here is my results, you need create a UIBezierPath and apply to your desired view, use this code for that
Function to generate the desired BezierPath
func pathSemiCirclesPathForView(givenView: UIView, ciclesRadius:CGFloat = 10, circlesDistance : CGFloat = 2) ->UIBezierPath
{
let width = givenView.frame.size.width
let height = givenView.frame.size.height
let semiCircleWidth = CGFloat(ciclesRadius*2)
let semiCirclesPath = UIBezierPath()
semiCirclesPath.move(to: CGPoint(x:0, y:0))
var x = CGFloat(0)
var i = 0
while x < width {
x = (semiCircleWidth) * CGFloat(i) + (circlesDistance * CGFloat(i))
let pivotPoint = CGPoint(x: x + semiCircleWidth/2, y: height)
semiCirclesPath.addArc(withCenter: pivotPoint, radius: ciclesRadius, startAngle: -180 * .pi / 180.0, endAngle: 0 * .pi / 180.0, clockwise: true)
semiCirclesPath.addLine(to: CGPoint(x: semiCirclesPath.currentPoint.x + circlesDistance, y: height))
i += 1
}
semiCirclesPath.addLine(to: CGPoint(x:width,y: 0))
i = 0
while x > 0 {
x = width - (semiCircleWidth) * CGFloat(i) - (circlesDistance * CGFloat(i))
let pivotPoint = CGPoint(x: x - semiCircleWidth/2, y: 0)
semiCirclesPath.addArc(withCenter: pivotPoint, radius: ciclesRadius, startAngle: 0 * .pi / 180.0, endAngle: -180 * .pi / 180.0, clockwise: true)
semiCirclesPath.addLine(to: CGPoint(x: semiCirclesPath.currentPoint.x - circlesDistance, y: 0))
i += 1
}
semiCirclesPath.close()
return semiCirclesPath
}
Function to apply the BezierPath to any View
func applySemiCircleEffect(givenView: UIView){
let shapeLayer = CAShapeLayer(layer: givenView.layer)
shapeLayer.path = self.pathSemiCirclesPathForView(givenView: givenView).cgPath
shapeLayer.frame = givenView.bounds
shapeLayer.masksToBounds = true
shapeLayer.shadowOpacity = 1
shapeLayer.shadowColor = UIColor.black.cgColor
shapeLayer.shadowOffset = CGSize(width: 0, height: 0)
shapeLayer.shadowRadius = 3
givenView.layer.mask = shapeLayer
}
Use it
#IBOutlet weak var customView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.applySemiCircleEffect(givenView: customView)
}
This is how it looks
Hope this helps you, Happy Coding

iOS Swift - Animate drawing of arc filled, not the border stroke

I am trying to Implement drawing of Arc from certain start angle to certain end angle. I am trying to animate drawing of arc but have no luck.
I have looked for couple of implementations but all of them refers to be drawing of animating boder of circle.
Like drawing a pie but with one color. With certain start and end angle. Animated drawing of pie.
What I like is to have have animated filling while arc is being animated from start angle to end angle.
Like arc is being drawn clockwise and while drawing it is filling its inside color.
Any idea How can I acheive this in Swift?
Thanks in advance.
Edit
This is what I have drawn:
(gray color shows area covered by drawing pie)
Code for drawing Pie
import Foundation
import UIKit
#IBDesignable class PieChart : UIView {
#IBInspectable var percentage: CGFloat = 50 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderWidth: CGFloat = 2 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var outerBorderColor: UIColor = UIColor.white {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var percentageFilledColor: UIColor = UIColor.primary {
didSet {
self.setNeedsDisplay()
}
}
let circleLayer = CAShapeLayer()
override func draw(_ rect: CGRect) {
drawPie(rect: rect, endPercent: percentage, color: UIColor.primary)
}
func drawPie(rect: CGRect, endPercent: CGFloat = 70, color: UIColor) {
let center = CGPoint(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2)
let radius = min(rect.width, rect.height) / 2
let gpath = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(360.degreesToRadians), clockwise: true)
UIColor.white.setFill()
gpath.fill()
let circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0), endAngle:CGFloat(360.degreesToRadians), clockwise: true)
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = outerBorderColor.cgColor
circleLayer.lineWidth = outerBorderWidth
circleLayer.borderColor = outerBorderColor.cgColor
self.layer.addSublayer(circleLayer)
let π: CGFloat = 3.14
let halfPi: CGFloat = π / 2
let startPercent: CGFloat = 0.0
let startAngle = (startPercent / 100 * π * 2 - π ) + halfPi
let endAngle = (endPercent / 100 * π * 2 - π ) + halfPi
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true)
path.close()
percentageFilledColor.setFill()
path.fill()
}
}
I've created this code using playground and swift. This code will draw a pie chart animating the filling color. Make sure you have the Timeline pane open on Xcode to see the animation working. I couldn't find a way to pass the path function directly to CAAnimation so my workaround was to create a list of steps, each one containing the path for a percentage stage. You can change the steps for a smother animation.
//: Playground - noun: a place where people can play
import UIKit
import XCPlayground
let STEPS_ANIMATION = 50
let initialPercentage : CGFloat = 0.10
let finalPercentage : CGFloat = 0.75
func buildPiePath(frame : CGRect, percentage : CGFloat) -> UIBezierPath {
let newPath = UIBezierPath()
let startPoint = CGPoint(x: frame.size.width, y: frame.height / 2.0)
let centerPoint = CGPoint(x: frame.size.width / 2.0, y: frame.height / 2.0)
let startAngle : CGFloat = 0
let endAngle : CGFloat = percentage * 2 * CGFloat(M_PI)
newPath.move(to: centerPoint)
newPath.addLine(to: startPoint)
newPath.addArc(withCenter: centerPoint,
radius: frame.size.width / 2.0,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
newPath.addLine(to: centerPoint)
newPath.close()
return newPath
}
func buildPiePathList(frame: CGRect,
startPercentage: CGFloat,
finalPercentage: CGFloat) -> [AnyObject] {
var listValues = [AnyObject]()
for index in 1...STEPS_ANIMATION {
let delta = finalPercentage - startPercentage
let currentPercentage = CGFloat(index) / CGFloat(STEPS_ANIMATION)
let percentage = CGFloat(startPercentage + (delta * currentPercentage))
listValues.append(buildPiePath(frame: frame,
percentage: percentage)
.cgPath)
}
return listValues
}
// Container for pie chart
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
container.backgroundColor = UIColor.gray
XCPShowView(identifier: "Container View", view: container)
let circleFrame = CGRect(x: 20, y: 20, width: 360, height: 360)
// Red background
let background = CAShapeLayer()
background.frame = circleFrame
background.fillColor = UIColor.red.cgColor
background.path = buildPiePath(frame: circleFrame, percentage: 1.0).cgPath
container.layer.addSublayer(background)
// Green foreground that animates
let foreground = CAShapeLayer()
foreground.frame = circleFrame
foreground.fillColor = UIColor.green.cgColor
foreground.path = buildPiePath(frame: circleFrame, percentage: initialPercentage).cgPath
container.layer.addSublayer(foreground)
// Filling animation
let fillAnimation = CAKeyframeAnimation(keyPath: "path")
fillAnimation.values = buildPiePathList(frame: circleFrame,
startPercentage: initialPercentage,
finalPercentage: finalPercentage)
fillAnimation.duration = 3
fillAnimation.calculationMode = kCAAnimationDiscrete
foreground.add(fillAnimation, forKey:nil)

Place UICollectionViewCells along an arc (UIBezierPath)

I would like to place UICollectionViewCells along the circular arc (UIBezierPath). Please suggest a formula to calculate center of these cells.
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
}
All these cells are of same constant size, say (100, 100) and x distance (horizontal) between two cells is also constant, say 20.
let startAngle: Double = 0
let endAngle: Double = 180
var arcRadius: CGFloat = 414 // Actually updated dynamically when layout changes
var arcCenter: CGPoint {
get {
return CGPoint(x: contentView.bounds.width/2, y: contentView.bounds.height/2 - arcRadius)
}
}
func drawArc() {
contentView.layer.sublayers?.forEach {
if $0 is CAShapeLayer {
$0.removeFromSuperlayer()
}
}
// Create an arc
let path = UIBezierPath(arcCenter: arcCenter,
radius: arcRadius,
startAngle: degreesToRadians(value: startAngle),
endAngle: degreesToRadians(value: endAngle),
clockwise: true)
// Create a shape layer and add path
let shapeLayer = createShapeLayer()
shapeLayer.path = path.cgPath
contentView.layer.addSublayer(shapeLayer)
}
It can be achieved using parametric equation of circle. Here is my code -
Create a Circle object and then call its func point(startAngle: forItemAtIndex:) to find the center point for these cells.
struct Circle {
var center: CGPoint
var radius: CGFloat
func point(startAngle: Double, forItemAtIndex index: Int) -> CGPoint {
/*
This is based on parametric equation of a circle with center (cx, cy) -
x = cx + radius*cos(angle)
y = cy + radius*sin(angle)
*/
var deltaX: CGFloat = 122 // Fixed horizontal (x) distance between 2 cells' centers
var deltaY = radius - sqrt(pow(radius, 2) - pow(deltaX, 2)) // formula to calculate vertical distance between cells' centers
var actualY = center.y + radius - deltaY
var deltaAngleRadians = asin((actualY - center.y)/radius)
var deltaAngle = radiansToDegrees(value: deltaAngleRadians)
var stepAngle: Double = deltaAngle - startAngle
var actualAngle = startAngle + (Double(index)*stepAngle)
let viewCenterX = radius * cos(degreesToRadians(value: actualAngle)) + center.x
let viewCenterY = radius * sin(degreesToRadians(value: actualAngle)) + center.y
var position = CGPoint(x: viewCenterX, y: viewCenterY)
return position
}
func bezierPath(startAngle: Double = 0, endAngle: Double = 180, clockwise: Bool = true) -> UIBezierPath {
return UIBezierPath(arcCenter: center, radius: radius, startAngle: degreesToRadians(value: startAngle), endAngle: degreesToRadians(value: endAngle), clockwise: true)
}
func degreesToRadians(value: Double) -> CGFloat {
return CGFloat(value * .pi / 180.0)
}
func radiansToDegrees(value: CGFloat) -> Double {
return Double(value * 180.0 / .pi)
}
}

How to draw a circle diagram looking like a clock face using UIKit

I've been trying to figure this out for too long. With the help of this blog I managed to draw the diagram itself, but it can't show me any data, because it seems like my idea of creating a context array is not possible and I can have only one context per view, is that right? So how can I change the color of each marker individually? I've seen the solution using SpriteKit, but I don't know anything at all about SpriteKit.
func degree2Radian(a:CGFloat)->CGFloat {
let b = CGFloat(M_PI) * a/180
return b
}
override func draw(_ rect: CGRect) {
color.set()
pathForCircleCenteredAtPoint(midPoint: circleCenter, withRadius: circleRadius).stroke()
color = UIColor.white
color.set()
pathForCircleCenteredAtPoint(midPoint: CGPoint(x: bounds.midX, y: bounds.midY), withRadius: circleRadius).fill()
color = UIColor(red: 0.93, green: 0.93, blue: 0.94, alpha: 1)
color.set()
let ctx = UIGraphicsGetCurrentContext()
for i in 0...100 {
secondMarkers(ctx: ctx!, x: circleCenter.x, y: circleCenter.y, radius: circleRadius - 4, sides: 100, color: color)
}
diagramArray[0].strokePath()
}
func degree2radian(a:CGFloat)->CGFloat {
let b = CGFloat(M_PI) * a/180
return b
}
func circleCircumferencePoints(sides:Int,x:CGFloat,y:CGFloat,radius:CGFloat,adjustment:CGFloat=0)->[CGPoint] {
let angle = degree2radian(a: 360/CGFloat(sides))
let cx = x // x origin
let cy = y // y origin
let r = radius // radius of circle
var i = sides
var points = [CGPoint]()
while points.count <= sides {
let xpo = cx - r * cos(angle * CGFloat(i)+degree2radian(a: adjustment))
let ypo = cy - r * sin(angle * CGFloat(i)+degree2radian(a: adjustment))
points.append(CGPoint(x: xpo, y: ypo))
i -= 1;
}
return points
}
func secondMarkers(ctx:CGContext, x:CGFloat, y:CGFloat, radius:CGFloat, sides:Int, color:UIColor) {
// retrieve points
let points = circleCircumferencePoints(sides: sides,x: x,y: y,radius: radius)
// create path
// determine length of marker as a fraction of the total radius
var divider:CGFloat = 1/16
//for p in points {
let path = CGMutablePath()
divider = 1/10
let xn = points[counter].x + divider * (x-points[counter].x)
let yn = points[counter].y + divider * (y-points[counter].y)
// build path
path.move(to: CGPoint(x: points[counter].x, y: points[counter].y))
//path, nil, p.x, p.y)
path.addLine(to: CGPoint(x: xn, y: yn))
//CGPathAddLineToPoint(path, nil, xn, yn)
path.closeSubpath()
// add path to context
ctx.addPath(path)
ctx.setStrokeColor(color.cgColor)
ctx.setLineWidth(2.0)
//ctx.strokePath()
diagramArray.append(ctx)
counter += 1
//}
// set path color
}
So basically I'm trying to append context for each marker to an array, but when I draw one element of this array, it draws the whole diagram. This is what I need to achieve.
You shouldn't need to create more than one CGContext - you should just be reusing the same one to draw all graphics. Also, your method to calculate the secondMarkers seems unnecessarily complex. I believe this does what you want:
private func drawTicks(context: CGContext, tickCount: Int, center: CGPoint, startRadius: CGFloat, endRadius: CGFloat, ticksToColor: Int) {
for i in 0 ... tickCount {
let color: UIColor = i < ticksToColor ? .blue : .lightGray
context.setStrokeColor(color.cgColor)
let angle = .pi - degree2Radian(a: (CGFloat(360.0) / CGFloat(tickCount)) * CGFloat(i))
let path = CGMutablePath()
path.move(to: circleCircumferencePoint(center: center, angle: angle, radius: startRadius))
path.addLine(to: circleCircumferencePoint(center: center, angle: angle, radius: endRadius))
context.addPath(path)
context.strokePath()
}
}
private func circleCircumferencePoint(center: CGPoint, angle: CGFloat, radius: CGFloat) -> CGPoint {
return CGPoint(x: radius * sin(angle) + center.x, y: radius * cos(angle) + center.y)
}

Resources