CAShapeLayer won't appear - swift

I can't for the life of me figure out why my CAShapeLayer is not appearing in my view. Here is my code:
let circleLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
setUpUI()
}
func setUpUI() {
let center = circleTimerView.center
let radius = circleTimerView.bounds.height / 2
let circularPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat.pi/2, endAngle: -(3/2)*CGFloat.pi, clockwise: true)
circleLayer.path = circularPath.cgPath
circleLayer.frame = circleTimerView.frame
circleLayer.fillColor = UIColor.gray.cgColor
circleLayer.strokeColor = UIColor.white.cgColor
circleLayer.lineWidth = 4
circleLayer.strokeEnd = 0
circleTimerView.layer.addSublayer(circleLayer)
}
The circleTimerView appears on my screen, but there is no sign of the layer at all. What am I missing?

Just figured this out, the endAngle of my CGPath was less than my start angle, so it wasn't being drawn. I changed the endAngle to (5/2)*CGFloat.pi and it worked perfectly. Matt was also right that strokeEnd can't be set to zero, I had that because I was going to animate the strokeEnd paramenter.

Related

WRONG LINE LENGHT shapeLayer.strokeEnd = 0.5 draws more than half of the circle [duplicate]

This question already has an answer here:
Stroke of CAShapeLayer stops at wrong point?
(1 answer)
Closed 1 year ago.
I want the line to end at the top with shapeLayer.strokeEnd = 1.0 and get a circle. the line must end here
but she goes on, it just can't be seen
full circle
when i specify a value of 0.5 i want to get half of the circle but i get much more
half circle
My code:
View
public func createCircleLine(progress: CGFloat, color: UIColor, width: CGFloat) {
let radius = (min(bounds.width, bounds.height) - circleLineWidth) / 2
let center = min(bounds.width, bounds.height) / 2
let bezierPath = UIBezierPath(arcCenter: CGPoint(x: center, y: center),
radius: radius,
startAngle: -CGFloat.pi / 2,
endAngle: 2 * CGFloat.pi,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.fillColor = nil
shapeLayer.strokeColor = circleProgressLineColor.cgColor
shapeLayer.lineWidth = circleLineWidth
shapeLayer.lineCap = .round
shapeLayer.strokeEnd = progress
layer.addSublayer(shapeLayer)
}
ViewController
class ViewController: UIViewController {
#IBOutlet weak var progressView: CircleProgressView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(false)
progressView.createCircleLine(progress: 1.0, color: .green, width: 10)
} }
I don’t understand why I can’t get the correct line length, the coordinates are correct
can i get the correct line length without CABasicAnimation ()?
You need to change the endAngle of your circle to 3 * CGFloat.pi / 2, so it is a complete cycle with no overlapping. The current circle you have has π/2 (90 degrees) overlap
let bezierPath = UIBezierPath(arcCenter: CGPoint(x: center, y: center),
radius: radius,
startAngle: -CGFloat.pi / 2,
endAngle: 3 * CGFloat.pi / 2,
clockwise: true)

Custom shape to UISlider and update progress in Swift 5

I have checked some of the StackOverflow answers regarding custom UIView slider but using them I unable to make the slider like this. This makes a circle or half circle. I have figured out some library that makes circle slider using UIView but its not helpful to me so could anyone please help me out. How can I make slider like in below UIImage? Thanks!
You will probably just roll your own. (You obviously could search for third party implementations, but that would be out of scope for StackOverflow.) There are a lot of ways of tackling this, but the basic elements here are:
The pink arc for the overall path. Personally, I'd use a CAShapeLayer for that.
The white arc from the start to the current progress (measured from 0 to 1). Again, a CAShapeLayer would be logical.
The white dot placed at the spot of the current progress. Below I create a CALayer with white background and then apply a CAGradientLayer as a mask to that. You could also just create a UIImage for this.
In terms of how to set the progress, you would set the paths of the pink and white arcs to the same path, but just update the strokeEnd of the white arc. You would also adjust the position of the white dot layer accordingly.
The only complicated thing here is figuring out the center of the arc. In my example below, I calculate it with some trigonometry based upon the bounds of the view so that arc goes from lower left corner, to the top, and back down the the lower right corner. Or you might instead pass the center of the arc as a parameter.
Anyway, it might look like:
#IBDesignable
class ArcView: UIView {
#IBInspectable
var lineWidth: CGFloat = 7 { didSet { updatePaths() } }
#IBInspectable
var progress: CGFloat = 0 { didSet { updatePaths() } }
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
progress = 0.35
}
lazy var currentPositionDotLayer: CALayer = {
let layer = CALayer()
layer.backgroundColor = UIColor.white.cgColor
let rect = CGRect(x: 0, y: 0, width: lineWidth * 3, height: lineWidth * 3)
layer.frame = rect
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.white.cgColor, UIColor.clear.cgColor]
gradientLayer.type = .radial
gradientLayer.frame = rect
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
gradientLayer.locations = [0.5, 1]
layer.mask = gradientLayer
return layer
}()
lazy var progressShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = .round
shapeLayer.lineWidth = lineWidth
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
return shapeLayer
}()
lazy var totalShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = .round
shapeLayer.lineWidth = lineWidth
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = #colorLiteral(red: 0.9439327121, green: 0.5454334617, blue: 0.6426400542, alpha: 1)
return shapeLayer
}()
override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePaths()
}
}
// MARK: - Private utility methods
private extension ArcView {
func configure() {
layer.addSublayer(totalShapeLayer)
layer.addSublayer(progressShapeLayer)
layer.addSublayer(currentPositionDotLayer)
}
func updatePaths() {
let rect = bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
let halfWidth = rect.width / 2
let height = rect.height
let theta = atan(halfWidth / height)
let radius = sqrt(halfWidth * halfWidth + height * height) / 2 / cos(theta)
let center = CGPoint(x: rect.midX, y: rect.minY + radius)
let delta = (.pi / 2 - theta) * 2
let startAngle = .pi * 3 / 2 - delta
let endAngle = .pi * 3 / 2 + delta
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
progressShapeLayer.path = path.cgPath // white arc
totalShapeLayer.path = path.cgPath // pink arc
progressShapeLayer.strokeEnd = progress
let currentAngle = (endAngle - startAngle) * progress + startAngle
let dotCenter = CGPoint(x: center.x + radius * cos(currentAngle),
y: center.y + radius * sin(currentAngle))
currentPositionDotLayer.position = dotCenter
}
}
Above, I set the background color of the ArcView so you could see its bounds, but you would obviously set the background color to be transparent.
Now there are tons of additional features you might add (e.g. add user interaction so you could “scrub” it, etc.). See https://github.com/robertmryan/ArcView for example. But the key when designing this sort of stuff is to just break it down into its constituent elements, layering one on top of the other.
You can programmatically set the progress of the arcView to get it to change the current value between values of 0 and 1:
func startUpdating() {
arcView.progress = 0
var current = 0
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] timer in
current += 1
guard let self = self, current <= 10 else {
timer.invalidate()
return
}
self.arcView.progress = CGFloat(current) / 10
}
}
Resulting in:

Gradient Arc View Not Showing Correctly

i have a custom view that is a 85% of a circle ( Just Stroke ) with Gradient color . here is my code :
class ProfileCircle:UIView
{
override func draw(_ rect: CGRect)
{
let desiredLineWidth:CGFloat = 4 // your desired value
let arcCenter = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
let radius = self.bounds.midX
let circlePath = UIBezierPath(
arcCenter: arcCenter,
radius: radius,
startAngle: CGFloat(0),
endAngle:CGFloat((.pi * 2)*0.85),
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.lineCap = .round
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = desiredLineWidth
let gradient = CAGradientLayer()
gradient.frame = circlePath.bounds
gradient.colors = [UIColor.colorPrimary.cgColor, UIColor.colorSecondary.cgColor]
//layer.addSublayer(shapeLayer)
gradient.mask = shapeLayer
layer.addSublayer(gradient)
}
}
but the edges are clipped
i have tried a lot but i cant fix it .
one more thing is that when i change the radius manually the view wont be centered
Just two changes:
let radius = self.bounds.midX - desiredLineWidth
and
gradient.frame = self.bounds

Center a bezier path to a UIview?

For the life of me, I cannot center this bezier path (the circle displayed below). The orange view is properly constrained and horizontally aligned, however when I add 3 layers to the orange view using the method below, I cannot seem to center it to the orange view. I declared the orange view as spinnerHolder.
private func createCircleShapeLayer(strokeColor: UIColor, fillColor: UIColor) -> CAShapeLayer {
let layer = CAShapeLayer()
//The farther from 0 x is for this, the more separated the movements of the 3 paths.
let circularPath = UIBezierPath(
arcCenter: CGPoint(x: 0, y: 0),
radius: 30,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true
)
layer.path = circularPath.cgPath
layer.strokeColor = strokeColor.cgColor
layer.lineWidth = 3
layer.fillColor = fillColor.cgColor
layer.lineCap = kCALineCapRound
layer.position = spinnerHolder.center
return layer
}
1- Change arcCenter to
let circularPath = UIBezierPath(arcCenter: CGPoint(x:spinnerHolder.frame.width/2, y:spinnerHolder.frame.height/2),
radius: 30,
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
2- Comment this
layer.position = spinnerHolder.center
3- Call the method inside viewDidLayoutSubviews
var once = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if once {
spinnerHolder.addSublayer(createCircleShapeLayer(,,,,,))
once = false
}
}
calling inside viewDidLayoutSubviews isn't mandatory , add it anytime/anywhere but not before the VC loads
4- Check this Centering CAShapeLayer within UIView Swift
set arcCenter point as
let centerPoint = CGPoint(x:self.viewRect.bounds.midX, y: self.viewRect.bounds.midY)

Swift Creating an Inner Shadow on a round UIView

I'd really appreciate it, if someone could tell me why the code below does not give me the inner shadow, or give me a solution that will give an inner shadow.
I need to create an inner shadow on a rounded UIView. I've been through many answers and found ways of getting this on a normal squared UIViews, but have found no solution that works on a rounded view. Instead I find solutions like the one shown below that look ok to me, but do not create the required inner shadow when I implement them.
Here is my screen, it is the white view between the outer blue and inner yellow views that I want to add the shadow to:
I have subclassed the view, here is my draw rect code:
let innerShadow = CALayer()
// Shadow path (1pt ring around bounds)
let path = UIBezierPath(rect: innerShadow.bounds.insetBy(dx: -1, dy: -1))
let cutout = UIBezierPath(rect: innerShadow.bounds).reversing()
path.append(cutout)
innerShadow.shadowPath = path.cgPath
innerShadow.masksToBounds = true
// Shadow properties
innerShadow.shadowColor = UIColor.darkGray.cgColor
innerShadow.shadowOffset = CGSize(width: 0.0, height: 7.0)
innerShadow.shadowOpacity = 1
innerShadow.shadowRadius = 5
// Add
self.layer.addSublayer(innerShadow)
// Make view round
self.layer.cornerRadius = self.frame.size.width/2
self.layer.masksToBounds = true
Many thanks for any help with this. Please do let me know if you have questions.
Just found this out yesterday
Mask out a circle from the centre of the blue view
let maskLayer = CAShapeLayer()
// based on the image the ring is 1 / 6 its diameter
let radius = self.bounds.width * 1.0 / 6.0
let path = UIBezierPath(rect: self.bounds)
let holeCenter = CGPoint(x: center.x - (radius * 2), y: center.y - (radius * 2))
path.addArc(withCenter: holeCenter, radius: radius, startAngle: 0, endAngle: CGFloat(2 * Double.pi), clockwise: true)
maskLayer.path = path.cgPath
maskLayer.fillRule = kCAFillRuleEvenOdd
blueView.layer.mask = maskLayer
The above will give you a blue ring.
Next create a blackView that will act as our shadow
var blackView = UIView()
Set its frame to be the same as the blue view.
blackView.frame = blueView.frame
blackView.clipToBounds = true
Cut out a similar hole from the blackView
let maskLayer = CAShapeLayer()
// This is the most important part, the mask shadow allows some of the black view to bleed from under the blue view and give a shadow
maskLayer.shadowOffset = CGSize(width: shadowX, height: shadowY)
maskLayer.shadowRadius = shadowRadius
let radius = self.bounds.width * 2.0 / 6.0
let path = UIBezierPath(rect: self.bounds)
let holeCenter = CGPoint(x: center.x - (radius * 2), y: center.y - (radius * 2))
path.addArc(withCenter: holeCenter, radius: radius, startAngle: 0, endAngle: CGFloat(2 * Double.pi), clockwise: true)
maskLayer.path = path.cgPath
maskLayer.fillRule = kCAFillRuleEvenOdd
blackView.layer.mask = maskLayer
Drop-in subclass of UIView inspired by PaintCode app.
class ShadowView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.groupTableViewBackground
let lbl = UILabel(frame: .zero)
addSubview(lbl)
lbl.translatesAutoresizingMaskIntoConstraints = false
lbl.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
lbl.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
lbl.text = "Text Inside"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
innerShadowOval(frame: rect)
}
func innerShadowOval(frame: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
// oval color
let color = UIColor.clear
// shadow setup
let shadow = NSShadow()
shadow.shadowColor = UIColor.black
shadow.shadowOffset = CGSize(width: 2, height: 0)
shadow.shadowBlurRadius = 3
// oval path
let ovalPath = UIBezierPath(ovalIn: frame)
color.setFill()
ovalPath.fill()
// oval inner shadow
context.saveGState()
context.clip(to: ovalPath.bounds)
context.setShadow(offset: CGSize.zero, blur: 0)
context.setAlpha((shadow.shadowColor as! UIColor).cgColor.alpha)
context.beginTransparencyLayer(auxiliaryInfo: nil)
let ovalOpaqueShadow = (shadow.shadowColor as! UIColor).withAlphaComponent(1)
context.setShadow(offset: CGSize(width: shadow.shadowOffset.width,
height: shadow.shadowOffset.height),
blur: shadow.shadowBlurRadius,
color: ovalOpaqueShadow.cgColor)
context.setBlendMode(.sourceOut)
context.beginTransparencyLayer(auxiliaryInfo: nil)
ovalOpaqueShadow.setFill()
ovalPath.fill()
context.endTransparencyLayer()
context.endTransparencyLayer()
context.restoreGState()
context.restoreGState()
}
}
And here goes the result