How to make a gradient circular path in Swift - ios

I want to create a gradient circular path like the following image:
and I have the following code:
circularPath = UIBezierPath(arcCenter: .zero, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
outerTrackShapeLayer = CAShapeLayer()
outerTrackShapeLayer.path = circularPath.cgPath
outerTrackShapeLayer.position = position
outerTrackShapeLayer.strokeColor = outerTrackColor.cgColor
outerTrackShapeLayer.fillColor = UIColor.clear.cgColor
outerTrackShapeLayer.lineWidth = lineWidth
outerTrackShapeLayer.strokeEnd = 1
outerTrackShapeLayer.lineCap = CAShapeLayerLineCap.round
outerTrackShapeLayer.transform = rotateTransformation
addSublayer(outerTrackShapeLayer)
innerTrackShapeLayer = CAShapeLayer()
innerTrackShapeLayer.strokeColor = innerTrackColor.cgColor
innerTrackShapeLayer.position = position
innerTrackShapeLayer.strokeEnd = progress
innerTrackShapeLayer.lineWidth = lineWidth
innerTrackShapeLayer.lineCap = CAShapeLayerLineCap.round
innerTrackShapeLayer.fillColor = UIColor.clear.cgColor
innerTrackShapeLayer.path = circularPath.cgPath
innerTrackShapeLayer.transform = rotateTransformation
let gradient = CAGradientLayer()
gradient.frame = circularPath.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]
gradient.position = innerTrackShapeLayer.position
gradient.mask = innerTrackShapeLayer
addSublayer(gradient)
but it doesn't work correctly, you can see the result in the following image:
I would appreciate if someone help me, thanks.

It looks like the gradient layer frame is set equal to the path frame which doesn't include the thickness of the stroke of the CAShapeLayer which is why it is cut off in a square. I can't see from the code whether you have the circular path on a subview but if you set the gradient frame to the same as the subview frame that should sort the cut off out as well as the misalignment of the progress view on the track.
Hope that helps.

I also face this issue and fixed it by changing the radius value as my radius value was larger than the expected
In circularPath, you need to make sure that your radius value should not be larger than the following
let radius = (min(width, height) - lineWidth) / 2
circularPath = UIBezierPath(arcCenter: .zero, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true)

Related

Center of UIView not centered

I have a CAShapeLayer that I am adding to the center of a UIView, but instead of appearing in the center, it appears in the top left corner of the UIView.
Here's the view setup:
let trayProgress: UIView = {
let view = UIView()
view.backgroundColor = .yellow
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: view.center, radius: 20, startAngle: -CGFloat.pi / 2, endAngle: CGFloat.pi * 2, clockwise: false)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 6
trackLayer.fillColor = UIColor.clear.cgColor
view.layer.addSublayer(trackLayer)
return view
}()
Here's what is showing in the view (I expect the gray circle to be in the center of the yellow view):
I have also tried to replace view.center with CGPoint(x: view.center.x, y: view.center.y), but I get the same result.
Can anyone tell me why this is happening and how I can fix it? Thanks!
The problem is in part this line:
arcCenter: view.center
That makes no sense, because the view’s center is where it is in its superview. You want to make the shape layer’s frame the same as the view’s bounds! The arc center should then be the shape layer’s own center (which, because it is a layer, is its position).
But another part of the problem is that in the code you’ve shown neither the view nor the shape layer has any size. You cannot create the shape layer until the view has size.
Example (I have starred the key changes in the code):
let trayProgress: UIView = {
let view = UIView(frame:CGRect(x: 30, y: 60, width: 50, height: 50)) // *
view.backgroundColor = .yellow
let trackLayer = CAShapeLayer()
trackLayer.frame = view.bounds // *
let circularPath = UIBezierPath(arcCenter: trackLayer.position, // *
radius: 20, startAngle: -CGFloat.pi / 2, endAngle: CGFloat.pi * 2, clockwise: false)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 6
trackLayer.fillColor = UIColor.clear.cgColor
view.layer.addSublayer(trackLayer)
return view
}()
Result:
You can tweak that, of course. For example you can move the center of the view to wherever you like.

CAShapeLayer with gradient

I have a problem with adding gradient to CAShapeLayer. I've created circle layer and I want to add a gradient to it. So, I created a CAGradientLayer and set it frames to layer bounds and mask to layer and it doesn't appear. What can I do?
private func setupCircle() -> CAShapeLayer {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: progressView.frame.size.width / 2.0, y: progressView.frame.size.height / 2.0), radius: (progressView.frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.CGPath
circleLayer.fillColor = nil
circleLayer.strokeColor = UIColor.yellowColor().CGColor
circleLayer.backgroundColor = UIColor.yellowColor().CGColor
circleLayer.lineWidth = configurator.lineWidth
circleLayer.strokeEnd = 1.0
return circleLayer
}
private func setupGradient() -> CAGradientLayer {
let gradient = CAGradientLayer()
gradient.colors = [UIColor.clearColor().CGColor, UIColor.yellowColor().CGColor, UIColor.greenColor().CGColor]
gradient.locations = [0.0, 0.25, 1.0]
gradient.startPoint = CGPointMake(0, 0)
gradient.endPoint = CGPointMake(1, 1)
return gradient
}
let circle = setupCircle()
let gradient = setupGradient()
gradient.frame = circle.bounds
gradient.mask = circle
progressView.layer.addSublayer(circle)
progressView.layer.insertSublayer(gradient, atIndex: 0)
EDIT:
I've changed setupCircle method to:
private func setupCircle() -> CAShapeLayer {
let circleLayer = CAShapeLayer()
circleLayer.frame = progressView.bounds.insetBy(dx: 5, dy: 5)
circleLayer.path = UIBezierPath(ovalInRect: CGRectMake(progressView.center.x, progressView.center.y, progressView.bounds.size.width, progressView.bounds.size.height)).CGPath
circleLayer.fillColor = nil
circleLayer.strokeColor = UIColor.yellowColor().CGColor
circleLayer.backgroundColor = UIColor.yellowColor().CGColor
circleLayer.lineWidth = configurator.lineWidth
circleLayer.strokeEnd = 1.0
return circleLayer
}
And here the effect, did I do it properly?:
EDIT 2:
I've changed circleLayer.path = UIBezierPath(ovalInRect: CGRectMake(progressView.center.x, progressView.center.y, progressView.bounds.size.width, progressView.bounds.size.height)).CGPath to circleLayer.path = UIBezierPath(ovalInRect: circleLayer.bounds).CGPath and it's better but still not what I want:
It's ok that you create layers and add them to layer hierarchy in viewDidLoad, but you shouldn't do any frame logic there since layout is not updated at that moment.
Try to move all the code which is doing any frame calculations to viewDidLayoutSubviews.
This should also be done every time layout is updated, so move it from setupCircle to viewDidLayoutSubviews:
let circlePath = UIBezierPath(...)
circleLayer.path = circlePath.cgPath
Edit:
Also I can't see where do you set circleLayer's frame? It's not enough to set it's path. If you have CGRect.zero rect for a mask, it will completely hide it's parent layer.
I would recommend to replace this:
let circlePath = UIBezierPath(arcCenter: CGPoint(x: progressView.frame.size.width / 2.0, y: progressView.frame.size.height / 2.0), radius: (progressView.frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
with this:
circleLayer.bounds = progressView.frame.insetBy(dx: 5, dy: 5)
circleLayer.path = UIBezierPath(ovalIn: circleLayer.bounds).cgPath
This way you'll have both frame and path set. Also be careful about coordinate system. I'm not sure about where is progressView in your view hierarchy, perhaps you'll need it's bounds instead.
Edit 2:
Also you should delete a few lines of code there.
This makes your circle layer completely opaque, it makes the shape itself unvisible.
circleLayer.backgroundColor = UIColor.yellowColor().CGColor
You shouldn't add circle as a sublayer since you're going to use it as a mask:
progressView.layer.addSublayer(circle)

How to find radius of image view?

I currently have an image view that contains a circular image.
I've set it up like so:
profileImageView.layer.cornerRadius = self.profileImageView.frame.size.width / 2
profileImageView.clipsToBounds = true
I'm attempting to draw an arc around the circle using UIBezierPath, and I would like to pass the radius of the Image View for the radius parameter.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: profileImageView.frame.size.width/2, y: profileImageView.frame.size.height/2), radius: IMG_VIEW_RADIUS, startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
How would I go about doing that?
Swift 3.0
Another way
I just added a imageView like this
let imageView = UIImageView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
imageView.backgroundColor = UIColor.green
imageView.layer.cornerRadius = imageView.frame.size.width / 2
imageView.clipsToBounds = true
self.view.addSubview(imageView)
Doing the circular bezier path
let circlePath = UIBezierPath(arcCenter: CGPoint(x: imageView.frame.size.width/2,y: imageView.frame.size.height/2), radius: CGFloat((imageView.frame.size.width/2) - 3.5), startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
//fill color
shapeLayer.fillColor = UIColor.clear.cgColor
//stroke color
shapeLayer.strokeColor = UIColor.white.cgColor
//line width
shapeLayer.lineWidth = 2.0
//finally adding the shapeLayer to imageView's layer
imageView.layer.addSublayer(shapeLayer)
Now creating an outside border using the same concept
let outerCirclePath = UIBezierPath(arcCenter: CGPoint(x: imageView.frame.size.width/2,y: imageView.frame.size.height/2), radius: CGFloat(imageView.frame.size.width/2 ), startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
let outerLayer = CAShapeLayer()
outerLayer.path = outerCirclePath.cgPath
//fill color
outerLayer.fillColor = UIColor.clear.cgColor
//stroke color
outerLayer.strokeColor = UIColor.blue.cgColor
//line width
outerLayer.lineWidth = 15.0
imageView.layer.addSublayer(outerLayer)
Now change the zPosition of shape layer created for the inner layer as the radius of this is smaller than the outer layer and it should be added at the top in order to be visible
shapeLayer.zPosition = 2
You need to tweak a bit with the radius of the first inner layer. In my case I just subtracted the radius with 3.5
just use border width and border color
profileImageView?.layer.cornerRadius = 5.0
profileImageView?.layer.borderColor = UIColor.white.cgColor

Strange behaviour when animating between two circular UIBezierPaths

The Problem
I am creating an exploding rings effect. I'm using multiple CAShapeLayer's and creating a UIBezierPath for each layer. When the view is initialised, all the layers have a path with width 0. When an action is triggered, the rings animate to a larger path.
As seen in the demo below, the right hand edge of each layer is slower to animate than the remainder of each circular layer.
Demo
Code
Drawing the layers:
func draw(inView view: UIView) -> CAShapeLayer {
let shapeLayer = CAShapeLayer()
// size.width defaults to 0
let circlePath = UIBezierPath(arcCenter: view.center, radius: size.width / 2, startAngle: 0, endAngle: CGFloat(360.0).toRadians(), clockwise: true)
shapeLayer.path = circlePath.CGPath
shapeLayer.frame = view.frame
return shapeLayer
}
Updating the layers
func updateAnimated(updatedOpacity: CGFloat, updatedSize: CGSize, duration: CGFloat) {
let updatePath = UIBezierPath(arcCenter: view.center, radius: updatedSize.width / 2,
startAngle: 0, endAngle: CGFloat(360).toRadians(), clockwise: true)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.fromValue = layer.path // the current path (initially with width 0)
pathAnimation.toValue = updatePath.CGPath
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = self.opacity
opacityAnimation.toValue = updatedOpacity
let animationGroup = CAAnimationGroup()
animationGroup.animations = [pathAnimation, opacityAnimation]
animationGroup.duration = Double(duration)
animationGroup.fillMode = kCAFillModeForwards
animationGroup.removedOnCompletion = false
layer.addAnimation(animationGroup, forKey: "shape_update")
... update variables ...
}
Avoid starting with a path width of 0. It is very difficult to scale from 0 and tiny floating-point errors magnify. Values very close to zero are not very continuous in floating point (hmmm.... I guess that actually is a little like the real universe). Try starting with larger values and see how close to zero you can go safely.

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