Circular loading bar with image - ios

I want to create an achievement system in my game. I want players to see their % achievement progress like this.
For example, if he completed 65% -> 75% -> 100% to look like this.
https://imgur.com/a/P3l6hOQ
Is there any way to crop the image circular to look like a circular progess bar? A direction, a hint, a pod or a piece of code is appreciated.
My images will be rectangles with rounded corners and the code is written in SpriteKit.

Managed to solve it. Indeed the best and easiest solution was to create a mask_node.
let button_image : SKSpriteNode = SKSpriteNode()
let crop_node : SKCropNode = SKCropNode()
let completion : CGFloat = 0.83 // from 0.0 to 1.0
let circle_bezier_path = UIBezierPath(arcCenter: CGPoint.zero, radius: 200, startAngle: 0, endAngle: CGFloat.pi * 2 * completion, clockwise: true)
circle_bezier_path.addLine(to: CGPoint(x: 0, y: 0))
let mask_node : SKShapeNode = SKShapeNode(path: circle_bezier_path.cgPath)
mask_node.zRotation = CGFloat.pi/2
let wrapper_node = SKNode()
wrapper_node.addChild(mask_node)
crop_node.maskNode = wrapper_node
crop_node.addChild(button_image)
addChild(crop_node)
Good luck!

Related

Semi circle between 2 CGPoints

I want to draw a semi circle between 2 points on a circle. The main represents a clock and i want to draw another line to represent a progress from one hour to another so the points position may vary. First of all i know the X and Y of the 2 points i am interested in. This is how i try to add angles in UIBezierPath. My problem is that the new circle starts correctly but ends at a totally random location
let firstAngle = atan2(redPoint.y - circleCenter.y, redPoint.x - circleCenter.x)
let secondAngle = atan2(bluePoint.y - circleCenter.y, bluePoint.x - circleCenter.x) ```
let circlePath1 = UIBezierPath(arcCenter: circleCenter,
radius: circleRadius,
startAngle: firstAngle,
endAngle: secondAngle,
clockwise: true) ```
Wherever i set the redPoint, the circle starts at a correct location but the circle never ands at bluePoint.
I have tried your code and it works for me if points are actually on that circumference
let redPoint = CGPoint(x: 100.0, y: 200.0)
let bluePoint = CGPoint(x: 100.0, y: 0.0)
let circleCenter = CGPoint(x: 100.0, y: 100.0)
let circleRadius = CGFloat(100.0)
let firstAngle = atan2(redPoint.y - circleCenter.y, redPoint.x - circleCenter.x)
let secondAngle = atan2(bluePoint.y - circleCenter.y, bluePoint.x - circleCenter.x)
let path = UIBezierPath(arcCenter: circleCenter,
radius: circleRadius,
startAngle: firstAngle,
endAngle: secondAngle,
clockwise: true)
//Path in layer
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
self.view.layer.addSublayer(shapeLayer)
or for example:
let redPoint = CGPoint(x: 200.0, y: 100.0)
let bluePoint = CGPoint(x: 100.0, y: 0.0)
let circleCenter = CGPoint(x: 100.0, y: 100.0)
let circleRadius = CGFloat(100.0)
You get:
But if you use coordinates that are not actually on your circumference, you get wrong results. My suggestion is to check your inputs and eventually if points are belonging to your desired circumference or not.
You're doing this backwards. Don't try to get the angle by starting with the location of the little red and blue filled circles. Use the angle to place the little red and blue filled circles.
In that example, my angles are -1 and 2.8 (radians). First I draw the arc (just as you did); then I superimpose the circles, which is trivial because I know where their centers are (the endpoints of the arc) by converting polar to cartesian coordinates.

How I can create this circular shape in iOS Swift4?

I have to create this shape like the below image in a UIView. But I am not get any idea how to draw this shape. I am trying to draw this shape using UIBezierPath path with CAShapeLayer the circular line draw successfully but I am unable to draw the circular points and circular fill colour. Can anyone please suggest me how I can achieve this type shape using any library or UIBezierPath.
This is my code which I am using try to draw this circular shape.
class ViewController: UIViewController {
var firstButton = UIButton()
var mylabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
self.creatingLayerWithInformation()
}
func creatingLayerWithInformation(){
let safeAreaHeight = self.view.safeAreaInsets.top
let navBarHeight = self.navigationController?.navigationBar.frame.height
self.addLayer(isClockWise: true, radius: self.view.frame.width * 0.72, xPoint: 0, yPoint: navBarHeight!, layerColor: UIColor.green, fillcolor: .clear)
self.addLayer(isClockWise: true, radius: self.view.frame.width * 0.72, xPoint: self.view.frame.width, yPoint: self.view.frame.height - 150, layerColor: UIColor.blue, fillcolor: .clear)
let aa = self.view.frame.width * 0.72
self.addLayer(isClockWise: true, radius: 10, xPoint: 0+aa, yPoint: navBarHeight!+5, layerColor: UIColor.blue, fillcolor: .clear)
self.addLayer(isClockWise: true, radius: 10, xPoint: 0+15, yPoint: navBarHeight!+aa, layerColor: UIColor.blue, fillcolor: .clear)
}
func addLayer(isClockWise: Bool, radius: CGFloat, xPoint: CGFloat, yPoint: CGFloat, layerColor: UIColor, fillcolor: UIColor) {
let pi = CGFloat(Float.pi)
let start:CGFloat = 0.0
let end :CGFloat = 20
// circlecurve
let path: UIBezierPath = UIBezierPath();
path.addArc(
withCenter: CGPoint(x:xPoint, y:yPoint),
radius: (radius),
startAngle: start,
endAngle: end,
clockwise: isClockWise
)
let layer = CAShapeLayer()
layer.lineWidth = 3
layer.fillColor = fillcolor.cgColor
layer.strokeColor = layerColor.cgColor
layer.path = path.cgPath
self.view.layer.addSublayer(layer)
}}
But I am getting the below result.
Please suggest me how I can achieve this shape. Correct me if I am doing anything wrong. If there is any library present then also please suggest. Please give me some solution.
Advance thanks to everyone.
There are many ways to achieve this effect, but a simple solution is to not draw the large circle as a single arc, but rather as a series of arcs that start and stop at the edges of the smaller circles. To do this, you need to know what the offset is from the inner circles. Doing a little trigonometry, you can calculate that as:
let angleOffset = asin(innerRadius / 2 / mainRadius) * 2
Thus:
let path = UIBezierPath()
path.move(to: point(from: arcCenter, radius: mainRadius, angle: startAngle))
let anglePerChoice = (endAngle - startAngle) / CGFloat(choices.count)
let angleOffset = asin(innerRadius / 2 / mainRadius) * 2
var from = startAngle
for index in 0 ..< choices.count {
var to = from + anglePerChoice / 2 - angleOffset
path.addArc(withCenter: arcCenter, radius: mainRadius, startAngle: from, endAngle: to, clockwise: true)
to = from + anglePerChoice
from += anglePerChoice / 2 + angleOffset
path.move(to: point(from: arcCenter, radius: mainRadius, angle: from))
path.addArc(withCenter: arcCenter, radius: mainRadius, startAngle: from, endAngle: to, clockwise: true)
from = to
}
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = lineWidth
layer.addSublayer(shapeLayer)
Where:
func point(from point: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
return CGPoint(x: point.x + radius * cos(angle),
y: point.y + radius * sin(angle))
}
That yields:
So, when you then add the inner circles:
While the above is simple, it has limitations. Specifically if the lineWidth of the big arc was really wide in comparison to that of the small circles, the breaks in the separate large arcs won’t line up nicely with the edges of the small circles. E.g. imagine that the small circles had a radius 22 points, but that the big arc’s stroke was comparatively wide, e.g. 36 points.
If you have this scenario (not in your case, but for the sake of future readers), the other approach, as matt suggested, is to draw the big arc as a single stroke, but then mask it using the paths for the small circles.
So, imagine that you had:
a single CAShapeLayer, called mainArcLayer, for the big arc; and
an array of UIBezierPath, called smallCirclePaths, for all of the small circles.
You could then create a mask in layoutSubviews of your UIView subclass (or viewDidLayoutSubviews in your UIViewController subclass) like so:
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(rect: bounds)
smallCirclePaths.forEach { path.append($0) }
let mask = CAShapeLayer()
mask.fillColor = UIColor.white.cgColor
mask.strokeColor = UIColor.clear.cgColor
mask.lineWidth = 0
mask.path = path.cgPath
mask.fillRule = .evenOdd
mainArcLayer.mask = mask
}
That yields:
This is a slightly more generalized solution to this problem.
Think about the problem in the simplest possible form. Imagine a straight line with one small circle superimposed over the middle of it.
Let's divide our thinking into three layers:
The background that needs to show through
The layer that holds the straight line
The layer that holds the small circle
Calculate the location of the small circle layer relative to the straight line layer.
Place the small circle layer at that location. Okay, but the straight line shows through.
Go back to the straight line layer. Give it a mask. Construct that mask with a transparent circle at exactly the location of the small circle layer.
Now the mask "punches a hole" through the straight line — at exactly the place where the circle covers it. Thus we appear to see through the circle to the background, because the straight line is missing at exactly that place.
In real life there will be multiple circle layers and the mask will have multiple transparent circles and the straight line will be a curve, but that is a minor issue once you have the hole-punching worked out.

Swift and SpriteKit: how to implement non-fuzzy circle timer with SKShapeNode

The code below implements a circle timer with SKShapeNode where one slice of the circle vanishes as time ticks off.
Unfortunately, the timer is fuzzy because of limitations with SKShapeNode.
Answers like this did not address the issue, and also were old and may no longer apply.
1) Is there a way to use SKShapeNode without such fuzzy lines?
2) If there is no way to fix SKShapdeNode, what are the alternatives for implementing a similar circle timer?
let timer = SKShapeNode(circleOfRadius: CGFloat(50))
timer.fillColor = UIColor.red
timer.strokeColor = UIColor.clear
timer.lineWidth = 0
timer.zRotation = CGFloat.pi / 2
timer.path = self.circle(radius: CGFloat(50), percent: CGFloat(90))
fileprivate func circle(radius: CGFloat, percent: CGFloat) -> CGPath {
// Adjust <percent> if less than 0
let percent = percent < 0 ? 0 : percent
// Generate circle path
let start = CGFloat(0)
let end = CGFloat.pi * 2 * percent
let center = CGPoint.zero
let bezierPath = UIBezierPath()
bezierPath.move(to: center)
bezierPath.addArc(withCenter:center, radius: radius, startAngle: start, endAngle: end, clockwise: true)
bezierPath.addLine(to: center)
// Return path
return bezierPath.cgPath
}
Shaders will probably accomplish what you want, check out http://battleofbrothers.com/sirryan/understanding-shaders-in-spritekit/
Also, SKShapeNode should not be used outside of debugging according to AppleDocs, have ran into a few issues using them in the past.
#ElTomato mentioned setting a stroke color and line width, this will work in masking the fuzziness.
A better solution may be to just turn off anti-aliasing with isAntialiased = false
This would depend on your graphic preference of course.

circle from UIBezierPath drawn in wrong direction in SKShapeNode

Following UIBezierPath reference, I tried drawing a dashed path which should end up as dashed arc. However the drawing direction is wrong. clockwise was set to true but the top half of the circle was drawn as opposed to what was mentioned in the apple's page
let arcForCompleted =
UIBezierPath(arcCenter: origin, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: true)
let pattern = getPattern(self.circumference, segments: involved)
let dashedPathForCompleted = CGPathCreateCopyByDashingPath(arcForCompleted.CGPath, nil, 0, pattern, pattern.count)
let dashedCircleForCompleted = SKShapeNode(path: dashedPathForCompleted!)
I am guessing this is because UIKit and SpriteKit has different coordinate system.
UIBezierPath is written with UIKit in mind thus it uses the UIKit coordinate system, (0,0) is in the upper left with positive y values running down. For the SKNode it has different coordinate system, (0,0) is in the center with positive y running up. You should keep that in mind when drawing arcs as it will affect the clockwise parameter. You can find a discussion of SKNode coordinate system here.
You can paste this code in a playground to see the difference as well
let bezierPath = UIBezierPath(arcCenter: CGPoint(x: 50.0,y: 50.0), radius: 50, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: true)
class ArcView:UIView
{
override func drawRect(rect: CGRect) {
let arcForCompleted = bezierPath
let pattern:[CGFloat] = [10.0,10.0]
arcForCompleted.setLineDash(pattern, count: 2, phase: 0.0)
arcForCompleted.stroke()
}
}
let arcView = ArcView(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
arcView.backgroundColor = UIColor.whiteColor()
let arcForCompleted = bezierPath
let shape = SKShapeNode()
shape.path = arcForCompleted.CGPath

UIImageView Pie Mask in Swift 2

After searching numerous sources on how to mask an image in Swift, I've found many that utilize UIImage rather than UIImageView. It's getting quite confusing because they are almost similar yet they don't share some of the same subclasses & properties.
I have an image that will be added into a stacks view although I'd like this image to be masked with a circular shape with a fixed start and end angle.
Example:
Original Picture and Desired Mask Result
I'd like to emphasize that I am simulating a pie being sliced out so essentially I am cropping an image with the Pie's size.
Here's what I've attempted so far to get started (and I keep failing)...
let testPicture:UIImageView = UIImageView(image: UIImage(named: "myPicture"))
testPicture.contentMode = .ScaleAspectFit
testPicture.layer.borderWidth = 1
testPicture.clipsToBounds = true
testPicture.layer.masksToBounds = true
view.layer.addSublayer(shapeLayer)
// ???
// UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: clockwise).CGPath
self.myStackView.addArrangedSubview(testPicture)
self.myStackView.layoutIfNeeded()
I just can't figure out how to apply the same implementation of masking images with geometric functions when other sources out there solely use UIImage.
New Code (Attempt) to Generate Pie Mask
let center = CGPointMake(testPicture.frame.size.width / 2, testPicture.frame.size.height / 2)
let radius = CGFloat((CGFloat(testPicture.frame.size.width) - CGFloat(1.0)) / 2)
let midX = (testPicture.layer.frame.width-testPicture.layer.frame.width)/2
let midY = (testPicture.layer.frame.height-testPicture.layer.frame.height)/2
let circlePath = UIBezierPath(arcCenter: CGPoint(x: midX , y: midY), radius: radius, startAngle: CGFloat(10), endAngle:CGFloat(M_PI * 2), clockwise: true)
let maskLayer = CAShapeLayer()
maskLayer.path = circlePath.CGPath
testPicture.layer.mask = maskLayer
testPicture.clipsToBounds = true
testPicture.layer.masksToBounds = true
To mask a layer you assign a separate layer to the mask property. In this case, I think what you're looking for is:
testPicture.layer.mask = shapeLayer

Resources