filling a circle gradually from bottom to top swift2 - ios

Basically I'am very new to Swift 2 and have created a circle with a stroke and white background using below code, then I got a circle something like this:
func getDynamicItemQty() -> UIImage {
let View = UIView(frame: CGRectMake(0,0,200,200))
let circlePath =
UIBezierPath(arcCenter: CGPoint(x: 100,y: 100), radius: CGFloat(90), startAngle: CGFloat(9.4), endAngle:CGFloat(0), clockwise: false)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.CGPath
//shapeLayer.fillRule = kCAFillRuleEvenOdd
//change the fill color
shapeLayer.fillColor = UIColor.brownColor().CGColor
//you can change the stroke color
shapeLayer.strokeColor = UIColor.blueColor().CGColor
//you can change the line width
shapeLayer.lineWidth = 10
View.layer.addSublayer(shapeLayer)
return UIImage.renderUIViewToImage(View)
}
However, how can we draw circles that is partly filled horizontally in Swift 2? I mean circles which are filled, for example, from the bottom to the top according to the percentage specified in Swift code.
Here is a preview of what we need:

A view and a shape layer are definitely the wrang appraoch. You should take a look at UIGraphicsBeginImageContextWithOptions or for iOS 10 or newer UIGraphicsImageRenderer. For your problem: You should draw your circle twice. Something like that:
let size = CGSize(width: 200.0, height: 200.0)
UIGraphicsBeginImageContextWithOptions(size, true, 0)
let circlePath =
UIBezierPath(arcCenter: CGPoint(x: 100, y: 100), radius: CGFloat(90), startAngle: CGFloat(9.4), endAngle:CGFloat(0), clockwise: false)
UIColor.white.fill()
UIRectFill(origin: CGPoint.zero, size: size)
// Drawing the background with a clipping
UIGraphicsPushContext(UIGraphicsGetCurrentContext())
UIColor(...).setFill()
UIRectClip(CGRect(x: 0.0, y:10.0 + 180.0 * (1.0 - percentage), width:size.width, height:size.height))
circlePath.fill()
// leave the subcontext to discard the clipping
UIGraphicsPopContext()
UIColor(...).setStroke()
circlePath.lineWidth = 10.0
circlePath.stroke()
// Keep the fruits of our labour
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Related

Why does CAShapeLayer exist?

I'm trying to understand CAShapeLayer and where it shines. It seems like the main advantage is the shape style properties that you can conveniently access:
let shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: .init(width: 200, height: 200))
shapeLayer.fillColor = .none
shapeLayer.lineWidth = 5
shapeLayer.strokeColor = UIColor.blue.cgColor
let path = UIBezierPath()
let withCenter = CGPoint(x: 100, y: 100)
let radius = CGFloat(100)
let startAngle = CGFloat(0.0)
let endAngle = CGFloat(CGFloat.pi * 2)
path.addArc(withCenter: withCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
shapeLayer.path = path.cgPath
But, I think the advantage ends there. It rather limits your ability set the context to multiple states like colors for different paths and requires a new CAShapeLayer. I can override the drawing method or use the delegate, but that's be doing the same thing as CALayer in that case.
On the other hand, with CALayer:
let layer = CALayer()
layer.frame = CGRect(origin: CGPoint(x: 100, y: 300), size: .init(width: 200, height: 200))
let renderer = UIGraphicsImageRenderer(size: CGSize(width: 200, height: 200))
let image = renderer.image { (_) in
let path = UIBezierPath()
let withCenter = CGPoint(x: 100, y: 100)
let radius = CGFloat(100)
let startAngle = CGFloat(0.0)
let endAngle = CGFloat(CGFloat.pi * 2)
path.lineWidth = 5
UIColor.blue.setStroke()
path.addArc(withCenter: withCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.stroke()
}
layer.contents = image.cgImage
you can set multiple stroke colors for different paths. Obviously, you can set the CAShapeLayer's contents in a similar manner, but I want to take advantage of CAShapeLayer's API's that CALayer doesn't offer.
Are the shape style properties the main reason for CAShapeLayer?
For me, the big advantage to CAShapeLayer is animation. As Matt says, you can animate strokeStart and/or strokeEnd to cause a shape to either appear like it's being drawn with a pen, or disappear. You can also use animate strokeStart and/or strokeEnd to create a variety of different "wipe" animations.
Here is a link to a post I wrote using animations to strokeEnd to create a "clock wipe" animation:
How do you achieve a "clock wipe"/ radial wipe effect in iOS?
You can also animate the path that's installed into the shape layer. As long as the starting and ending paths have the same number of control points, the system creates a smooth, elegant looking animation. There are some tricks to making this work correctly however. Arc animations don't work as expected if you change the arc angle during an animation, because internally arcs are composed of different numbers of cubic Bezier curves.
Check out my projects RandomBlobs and TrochoidDemo on Github for examples of animating a path's control points.

Creating a thin black circle (unfilled) within a filled white circle (UIButton)

I'm trying to replicate the default camera button on iOS devices:
I'm able to create a white circular button with black button within it. However, the black button is also filled, instead of just being a thin circle.
This is what I have (most of it has been copied from different sources and put together, so the code isn't efficient)
The object represents the button,
func applyRoundCorner(_ object: AnyObject) {
//object.layer.shadowColor = UIColor.black.cgColor
//object.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
object.layer.cornerRadius = (object.frame.size.width)/2
object.layer.borderColor = UIColor.black.cgColor
object.layer.borderWidth = 5
object.layer.masksToBounds = true
//object.layer.shadowRadius = 1.0
//object.layer.shadowOpacity = 0.5
var CircleLayer = CAShapeLayer()
let center = CGPoint (x: object.frame.size.width / 2, y: object.frame.size.height / 2)
let circleRadius = object.frame.size.width / 6
let circlePath = UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: CGFloat(M_PI), endAngle: CGFloat(M_PI * 2), clockwise: true)
CircleLayer.path = circlePath.cgPath
CircleLayer.strokeColor = UIColor.black.cgColor
//CircleLayer.fillColor = UIColor.blue.cgColor
CircleLayer.lineWidth = 1
CircleLayer.strokeStart = 0
CircleLayer.strokeEnd = 1
object.layer.addSublayer(CircleLayer)
}
Basic Approach
You could do it like this (for the purpose of demonstration, I would do the button programmatically, using a playground):
let buttonWidth = 100.0
let button = UIButton(frame: CGRect(x: 0, y: 0, width: buttonWidth, height: buttonWidth))
button.backgroundColor = .white
button.layer.cornerRadius = button.frame.width / 2
Drawing Part:
So, after adding the button and do the desired setup (make it circular), here is part of how you could draw a circle in it:
let circlePath = UIBezierPath(arcCenter: CGPoint(x: buttonWidth / 2,y: buttonWidth / 2), radius: 40.0, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.black.cgColor
circleLayer.lineWidth = 2.5
// adding the layer into the button:
button.layer.addSublayer(circleLayer)
Probably, circleLayer.fillColor = UIColor.clear.cgColor is the part you missing 🙂.
Therefore:
Back to your case:
Aside Bar Tip:
For implementing applyRoundCorner, I would suggest to let it has only the job for rounding the view, and then create another function to add the circle inside the view. And that's for avoiding any naming conflict, which means that when reading "applyRoundCorner" I would not assume that it is also would add circle to my view! So:
func applyRoundedCorners(for view: UIView) {
view.layer.cornerRadius = view.frame.size.width / 2
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 5.0
view.layer.masksToBounds = true
}
func drawCircle(in view: UIView) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: view.frame.size.width / 2,y: view.frame.size.width / 2),
radius: view.frame.size.width / 2.5,
startAngle: 0,
endAngle: CGFloat.pi * 2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 2.5
button.layer.addSublayer(shapeLayer)
}
and now:
applyRoundedCorners(for: button)
drawCircle(in: button)
That's seems to be better. From another aspect, consider that you want to make a view to be circular without add a circle in it, with separated methods you could simply applyRoundedCorners(for: myView) without the necessary of adding a circle in it.
Furthermore:
As you can see, I changed AnyObject to UIView, it seems to be more logical to your case. So here is a cool thing that we could do:
extension UIView {
func applyRoundedCorners(for view: UIView) {
view.layer.cornerRadius = view.frame.size.width / 2
view.layer.borderColor = UIColor.white.cgColor
view.layer.borderWidth = 5.0
view.layer.masksToBounds = true
}
func drawCircle(in view: UIView) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: view.frame.size.width / 2,y: view.frame.size.width / 2),
radius: view.frame.size.width / 2.5,
startAngle: 0,
endAngle: CGFloat.pi * 2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 2.5
button.layer.addSublayer(shapeLayer)
}
}
Now both applyRoundedCorners and drawCircle are implicitly included to the UIView (which means UIButton), instead of passing the button to these functions, you would be able to:
button.applyRoundedCorners()
button.drawCircle()
You just need to add circle Shape layer with lesser width and height
Try this code
func applyRoundCorner(_ object: UIButton) {
object.layer.cornerRadius = (object.frame.size.width)/2
object.layer.borderColor = UIColor.black.cgColor
object.layer.borderWidth = 5
object.layer.masksToBounds = true
let anotherFrame = CGRect(x: 12, y: 12, width: object.bounds.width - 24, height: object.bounds.height - 24)
let circle = CAShapeLayer()
let path = UIBezierPath(arcCenter: object.center, radius: anotherFrame.width / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
circle.path = path.cgPath
circle.strokeColor = UIColor.black.cgColor
circle.lineWidth = 1.0
circle.fillColor = UIColor.clear.cgColor
object.layer.addSublayer(circle)
}
Note: Change frame value according to your requirements and best user experience
Output
I have no doubt there are a million different ways to approach this problem, this is just one...
I started with a UIButton for simplicity and speed, I might consider actually starting with a UIImage and simply setting the image properties of the button, but it would depend a lot on what I'm trying to achieve
internal extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
class RoundButton: UIButton {
override func draw(_ rect: CGRect) {
makeButtonImage()?.draw(at: CGPoint(x: 0, y: 0))
}
func makeButtonImage() -> UIImage? {
let size = bounds.size
UIGraphicsBeginImageContext(CGSize(width: size.width, height: size.height))
defer {
UIGraphicsEndImageContext()
}
guard let ctx = UIGraphicsGetCurrentContext() else {
return nil
}
let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
// Want to "over fill" the image area, so the mask can be applied
// to the entire image
let radius = min(size.width / 2.0, size.height / 2.0)
let innerRadius = radius * 0.75
let innerCircle = UIBezierPath(arcCenter: center,
radius: innerRadius,
startAngle: CGFloat(0.0).degreesToRadians,
endAngle: CGFloat(360.0).degreesToRadians,
clockwise: true)
// The color doesn't matter, only it's alpha level
UIColor.red.setStroke()
innerCircle.lineWidth = 4.0
innerCircle.stroke(with: .normal, alpha: 1.0)
let circle = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: CGFloat(0.0).degreesToRadians,
endAngle: CGFloat(360.0).degreesToRadians,
clockwise: true)
UIColor.clear.setFill()
ctx.fill(bounds)
UIColor.white.setFill()
circle.fill(with: .sourceOut, alpha: 1.0)
return UIGraphicsGetImageFromCurrentImageContext()
}
}
nb: This is unoptimised! I would consider caching the result of makeButtonImage and invalidate it when the state/size of the button changes, just beware of that
Why is this approach any "better" then any other? I just want to say, it's not, but what it does create, is a "cut out" of the inner circle
It's a nitpick on my part, but I think it looks WAY better and is a more flexible solution, as you don't "need" a inner circle stroke color, blah, blah, blah
The solution makes use of the CoreGraphics CGBlendModes
Of course I might just do the whole thing in PaintCodeApp and be done with it

Why does CGBlendMode.color work correctly in simulator but fail on device?

I'm using CAShapeLayer's render(in:) with blend mode set to CGBlendMode.color to tint an image drawn to the current CGContext. It works perfectly in the simulator, but not on the device.
Screenshot on the left is a simulated iPhone 7 Plus, on the right is the actual device running iOS 10.2. First the black square is drawn, then the red circle, then the CGBlendMode is set to color and the blue circle is drawn.
The documentation for CGBlendMode.color says:
Uses the luminance values of the background with the hue and
saturation values of the source image.
So where the blue circle overlaps the black background, it should use luminance of zero and draw black.
Why does this work correctly in the simulator but not on the real device? And more importantly, how can I get this type of blend to work on the device?
To reproduce, create a new project in Xcode 8.2.1 with the "Single View Application" template and replace ViewController.swift with:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let blendModeTestView = BlendModeTest(frame: CGRect(x: 100, y: 100, width: 250, height: 250))
view.addSubview(blendModeTestView)
}
}
class BlendModeTest: UIView {
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
// black background
let backgroundPath = UIBezierPath(rect: rect)
let backgroundShapeLayer = CAShapeLayer()
backgroundShapeLayer.path = backgroundPath.cgPath
backgroundShapeLayer.fillColor = UIColor.black.cgColor
backgroundShapeLayer.render(in: context)
// red circle
let circlePath = UIBezierPath(arcCenter: CGPoint(x: rect.size.width/2, y: rect.size.height/2),
radius: rect.size.width/4,
startAngle: CGFloat(0),
endAngle:CGFloat(M_PI * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.render(in: context)
// blue circle (with .color blend mode)
let colorFillPath = UIBezierPath(arcCenter: CGPoint.zero,
radius: rect.size.width * 0.8,
startAngle: CGFloat(0),
endAngle:CGFloat(M_PI * 2),
clockwise: true)
let colorFillShapeLayer = CAShapeLayer()
colorFillShapeLayer.path = colorFillPath.cgPath
colorFillShapeLayer.fillColor = UIColor.blue.cgColor
context.setBlendMode(.color)
colorFillShapeLayer.render(in: context)
}
}

Positioning layers of uiview in swift

I am working on a timeline view. I am drawing a line in the centre of my icon and drawing a circle with fill colour. But the problem is that when I draw the circle, it is always on the top of the icon. Now the icon is not showing. I tried zposition of the layers. Here is what I tried
override func draw(_ rect: CGRect) {
if let anchor = anchorView(){
let centreRelativeToTableView = anchor.superview!.convert(anchor.center, to: self)
// print("Anchor x origin : \(anchor.frame.size.origin.x)")
timelinePoint.position = CGPoint(x: centreRelativeToTableView.x , y: centreRelativeToTableView.y/2)
timeline.start = CGPoint(x: centreRelativeToTableView.x , y: 0)
timeline.middle = CGPoint(x: timeline.start.x, y: anchor.frame.origin.y)
timeline.end = CGPoint(x: timeline.start.x, y: self.bounds.size.height)
timeline.draw(view: self.contentView)
let circlePath = UIBezierPath(arcCenter: CGPoint(x: anchor.center.x,y: anchor.center.y - 20), radius: CGFloat(20), startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
//change the fill color
shapeLayer.fillColor = UIColor.randomFlat.cgColor
// shapeLayer.fillColor = UIColor.clear.cgColor
//you can change the stroke color
shapeLayer.strokeColor = UIColor.white.cgColor
//you can change the line width
shapeLayer.lineWidth = 5
shapeLayer.zPosition = 0
anchor.alpha = 1
//Set Anchor Z position
anchor.layer.zPosition = 2
shapeLayer.zPosition = 1
anchor.layer.addSublayer(shapeLayer)
// Setting icon to layer
/*let imageLayer = CALayer()
imageLayer.contents = newImage
anchor.layer.addSublayer(imageLayer)*/
// timelinePoint.draw(view: self.contentView)
}else{
print("this should not happen")
}
}
-------
I want to draw my icon with white tint on top of the circle. Please help me
Using addSublayer will add the new layer on top, which will cover everything else added before.
If you know which layer your icon is at, you can instead use insertSublayer(at:) to place it under your icon
Alternatively you can create another UIView with the exact frame of the icon in storyboard and place it lower in the hierarchy so whatever your draw ends up under.

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

Resources