Creating view with programmatic constraints causes the content to be drawn in the wrong place - swift

I am adding a custom view to another view who then has its constraints set programatically. This results in the content of the view being drawn in the wrong place, and I'm not sure why. Here's the code I have and what I tried so far:
// Called in viewDidLoad
let buttonView = UIView()
buttonView.translatesAutoresizingMaskIntoConstraints = false
let ring = CircularProgressView()
ring.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
ring.backgroundColor = .yellow
buttonView.addSubview(ring)
self.view.addSubview(buttonView)
NSLayoutConstraint.activate([
buttonView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
buttonView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -28),
buttonView.heightAnchor.constraint(equalToConstant: 80),
buttonView.widthAnchor.constraint(equalToConstant: 80),
ring.centerXAnchor.constraint(equalTo: buttonView.centerXAnchor),
ring.centerYAnchor.constraint(equalTo: buttonView.centerYAnchor)
])
I tried calling ring.layoutIfNeeded() or setting the width and height of the ring view with constraints, and it didn't change anything.
ring.heightAnchor.constraint(equalToConstant: 80),
ring.widthAnchor.constraint(equalToConstant: 80)
The CircularProgressView class looks like this. I tried using bounds instead of frame here or not halving the values by 2 and again no change.
class CircularProgressView: UIView {
// First create two layer properties
private var circleLayer = CAShapeLayer()
private var progressLayer = CAShapeLayer()
private var currentValue: Double = 0
override init(frame: CGRect) {
super.init(frame: frame)
createCircularPath()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createCircularPath()
}
func createCircularPath() {
let circularPath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0),
radius: 41,
startAngle: -.pi / 2,
endAngle: 3 * .pi / 2,
clockwise: true)
circleLayer.path = circularPath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.lineCap = .round
circleLayer.lineWidth = 3.0
circleLayer.strokeColor = UIColor.black.withAlphaComponent(0.2).cgColor
progressLayer.path = circularPath.cgPath
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineCap = .round
progressLayer.lineWidth = 3.0
progressLayer.strokeEnd = 0
progressLayer.strokeColor = UIColor.black.cgColor
layer.addSublayer(circleLayer)
layer.addSublayer(progressLayer)
let circularProgressAnimation = CABasicAnimation(keyPath: "strokeEnd")
circularProgressAnimation.duration = 2
circularProgressAnimation.toValue = 1
circularProgressAnimation.fillMode = .forwards
circularProgressAnimation.isRemovedOnCompletion = false
progressLayer.add(circularProgressAnimation, forKey: "progressAnim")
progressLayer.pauseAnimation()
}
}
This all works with no issue if done via the storyboard, but I need to create it programatically for this use case, and I'm a little confused about what's wrong.

You need to make both views programmatically like
// Called in viewDidLoad
let buttonView = UIView()
buttonView.translatesAutoresizingMaskIntoConstraints = false
let ring = CircularProgressView()
ring.translatesAutoresizingMaskIntoConstraints = false
ring.backgroundColor = .yellow
buttonView.addSubview(ring)
self.view.addSubview(buttonView)
NSLayoutConstraint.activate([
buttonView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
buttonView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -28),
buttonView.heightAnchor.constraint(equalToConstant: 80),
buttonView.widthAnchor.constraint(equalToConstant: 80),
ring.centerXAnchor.constraint(equalTo: buttonView.centerXAnchor),
ring.centerYAnchor.constraint(equalTo: buttonView.centerYAnchor),
ring.heightAnchor.constraint(equalToConstant: 80),
ring.widthAnchor.constraint(equalToConstant: 80)
])
and Call createCircularPath only inside layoutSubviews
override func layoutSubviews() {
super.layoutSubviews()
createCircularPath()
}

Try to also set CircularProgressView's translatesAutoresizingMaskIntoConstraints property to false:
ring.translatesAutoresizingMaskIntoConstraints = false

Related

make the uibutton rounded, shadow and gradient

can someone tell me please how to make the button rounded, shadow and gradient
here I set the gradient and shadow to the button, but without rounding:
#IBOutlet weak var info: UIButton!
info.setTitle("INFO", for: .normal)
info.setTwoGradients(colorOne: Colors.OrangeGrad, colorTwo: Colors.OrangeGradSec)
info.setTitleColor(UIColor.black, for: .normal)
info.layer.shadowColor = UIColor.darkGray.cgColor
info.layer.shadowOffset = CGSize(width: 2.0, height: 2.0)
info.layer.shadowOpacity = 0.8
info.layer.shadowRadius = 2
view.addSubview(info)
I know that the shadow disappears because of the rounding and I found a way to fix it, ex:
final class CustomButton: UIButton {
private var shadowLayer: CAShapeLayer!
override func layoutSubviews() {
super.layoutSubviews()
if shadowLayer == nil {
shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: 10).cgPath
shadowLayer.fillColor = UIColor.white.cgColor
shadowLayer.shadowColor = UIColor.darkGray.cgColor
shadowLayer.shadowPath = shadowLayer.path
shadowLayer.shadowOffset = CGSize(width: 5.0, height: 2.0)
shadowLayer.shadowOpacity = 0.7
shadowLayer.shadowRadius = 2
layer.insertSublayer(shadowLayer, at: 0)
}
}
but there is a line:
shadowLayer.fillColor = UIColor.white.cgColor
because of which I can't set the gradient
therefore, I cannot find a way by which all three conditions would be met
Output:
Usage
class ViewController: UIViewController {
#IBOutlet var btnGradient: CustomButton!
override func viewDidLoad() {
super.viewDidLoad()
btnGradient.gradientColors = [.red, .green]
btnGradient.setTitle("Gradient Button", for: .normal)
btnGradient.setTitleColor(.white, for: .normal)
}
}
Custom Class
Use this class as a reference to setup the attributes:
class CustomButton: UIButton {
var gradientColors : [UIColor] = [] {
didSet {
setupView()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
let startPoint = CGPoint(x: 0, y: 0.5)
let endPoint = CGPoint(x: 1, y: 0.5)
var btnConfig = UIButton.Configuration.plain()
btnConfig.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: layer.frame.height / 2, bottom: 5, trailing: layer.frame.height / 2)
self.configuration = btnConfig
layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
//Gradient
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = gradientColors.map { $0.cgColor }
gradientLayer.startPoint = startPoint
gradientLayer.endPoint = endPoint
gradientLayer.cornerRadius = layer.frame.height / 2
if let oldValue = layer.sublayers?[0] as? CAGradientLayer {
layer.replaceSublayer(oldValue, with: gradientLayer)
} else {
layer.insertSublayer(gradientLayer, below: nil)
}
//Shadow
layer.shadowColor = UIColor.darkGray.cgColor
layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.frame.height / 2).cgPath
layer.shadowOffset = CGSize(width: 2.0, height: 2.0)
layer.shadowOpacity = 0.7
layer.shadowRadius = 2.0
}
}
You can use UIButton Extension or refer code
Pass colors in array with start & end point of gradient effect, you want to start & end. i.e. x=0, y=0 means TopLeft & x=1, y=1 means BottomRight
extension UIButton {
func setGradientLayer(colorsInOrder colors: [CGColor], startPoint sPoint: CGPoint = CGPoint(x: 0, y: 0.5), endPoint ePoint: CGPoint = CGPoint(x: 1, y: 0.5)) {
let gLayer = CAGradientLayer()
gLayer.frame = self.bounds
gLayer.colors = colors
gLayer.startPoint = sPoint
gLayer.endPoint = ePoint
gLayer.cornerRadius = 5
gLayer.shadowOpacity = 0.8
gLayer.shadowOffset = CGSize(width: 2.0, height: 2.0)
layer.insertSublayer(gLayer, at: 0)
}
}

stackView content (SF-Symbols) distorted

I want to replicate the Apple Map App Buttons as seen here:
This is the code so far:
class CustomView: UIImageView {
init(frame: CGRect, corners: CACornerMask, systemName: String) {
super.init(frame: frame)
self.createBorders(corners: corners)
self.createImage(systemName: systemName)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createBorders(corners: CACornerMask) {
self.contentMode = .scaleAspectFill
self.clipsToBounds = false
self.layer.cornerRadius = 20
self.layer.maskedCorners = corners
self.layer.masksToBounds = false
self.layer.shadowOffset = .zero
self.layer.shadowColor = UIColor.gray.cgColor
self.layer.shadowRadius = 20
self.layer.shadowOpacity = 0.2
self.backgroundColor = .white
let shadowAmount: CGFloat = 2
let rect = CGRect(x: 0, y: 2, width: self.bounds.width + shadowAmount * 0.1, height: self.bounds.height + shadowAmount * 0.1)
self.layer.shadowPath = UIBezierPath(rect: rect).cgPath
}
func createImage(systemName: String) {
let image = UIImage(systemName: systemName)!
let renderer = UIGraphicsImageRenderer(bounds: self.frame)
let renderedImage = renderer.image { (_) in
image.draw(in: frame.insetBy(dx: 30, dy: 30))
}
And use it like this:
let size = CGSize(width: 100, height: 100)
let frame = CGRect(origin: CGPoint(x: 100, y: 100), size: size)
let infoView = CustomView(frame: frame, corners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], systemName: "info.circle")
let locationView = CustomView(frame: frame, corners: [], systemName: "location")
let twoDView = CustomView(frame: frame, corners: [.layerMinXMaxYCorner, .layerMaxXMaxYCorner], systemName: "view.2d")
let binocularsView = CustomView(frame: frame, corners: [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner], systemName: "binoculars.fill")
let stackView = UIStackView(arrangedSubviews: [infoView, locationView, twoDView, binocularsView])
stackView.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: 100, height: 400))
stackView.distribution = .fillEqually
stackView.axis = .vertical
stackView.setCustomSpacing(20, after: twoDView)
view.addSubview(stackView)
Which makes it look like this:
AND: There aren't the thin lines between each Icon of the stackView :/
Thanks for your help!
Not sure what's going on, because when I ran you code as-is I did not get the stretched images you've shown.
To get "thin lines between each Icon", you could either modify your custom image view to add the lines to the middle icon image, or, what might be easier, would be to use a 1-point height UIView between each icon.
Also, you'll be better off using auto-layout.
See if this gives you the desired result:
class CustomView: UIImageView {
init(frame: CGRect, corners: CACornerMask, systemName: String) {
super.init(frame: frame)
self.createBorders(corners: corners)
self.createImage(systemName: systemName)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createBorders(corners: CACornerMask) {
self.contentMode = .scaleAspectFill
self.clipsToBounds = false
self.layer.cornerRadius = 20
self.layer.maskedCorners = corners
self.layer.masksToBounds = false
self.layer.shadowOffset = .zero
self.layer.shadowColor = UIColor.gray.cgColor
self.layer.shadowRadius = 20
self.layer.shadowOpacity = 0.2
self.backgroundColor = .white
let shadowAmount: CGFloat = 2
let rect = CGRect(x: 0, y: 2, width: self.bounds.width + shadowAmount * 0.1, height: self.bounds.height + shadowAmount * 0.1)
self.layer.shadowPath = UIBezierPath(rect: rect).cgPath
}
func createImage(systemName: String) {
guard let image = UIImage(systemName: systemName)?.withTintColor(.blue, renderingMode: .alwaysOriginal) else { return }
let renderer = UIGraphicsImageRenderer(bounds: self.frame)
let renderedImage = renderer.image { (_) in
image.draw(in: frame.insetBy(dx: 30, dy: 30))
}
self.image = renderedImage
}
}
class TestCustomViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
let size = CGSize(width: 100, height: 100)
let frame = CGRect(origin: .zero, size: size)
let infoView = CustomView(frame: frame, corners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], systemName: "info.circle")
let locationView = CustomView(frame: frame, corners: [], systemName: "location")
let twoDView = CustomView(frame: frame, corners: [.layerMinXMaxYCorner, .layerMaxXMaxYCorner], systemName: "view.2d")
let binocularsView = CustomView(frame: frame, corners: [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner], systemName: "binoculars.fill")
// create 2 separator views
let sep1 = UIView()
sep1.backgroundColor = .blue
let sep2 = UIView()
sep2.backgroundColor = .blue
let stackView = UIStackView(arrangedSubviews: [infoView, sep1, locationView, sep2, twoDView, binocularsView])
// use .fill, not .fillEqually
stackView.distribution = .fill
stackView.axis = .vertical
stackView.setCustomSpacing(20, after: twoDView)
view.addSubview(stackView)
// let's use auto-layout
stackView.translatesAutoresizingMaskIntoConstraints = false
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// only need top and leading constraints for the stack view
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 100.0),
// both separator views need height constraint of 1.0
sep1.heightAnchor.constraint(equalToConstant: 1.0),
sep2.heightAnchor.constraint(equalToConstant: 1.0),
])
// set all images to the same 1:1 ratio size
[infoView, locationView, twoDView, binocularsView].forEach { v in
v.widthAnchor.constraint(equalToConstant: size.width).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
}
}
}
This is what I get with that code example:

Resize Image in stackView, keep aspect-ratio

I want to clone the Apple Maps App UIButtons, they look like this:
This is my Code:
class CustomView: UIImageView {
init(frame: CGRect, corners: CACornerMask, systemName: String) {
super.init(frame: frame)
self.createBorders(corners: corners)
self.createImage(systemName: systemName)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createBorders(corners: CACornerMask) {
self.contentMode = .scaleAspectFill
self.clipsToBounds = false
self.layer.cornerRadius = 10
self.layer.maskedCorners = corners
self.layer.masksToBounds = false
self.layer.shadowOffset = .zero
self.layer.shadowColor = UIColor.gray.cgColor
self.layer.shadowRadius = 10
self.layer.shadowOpacity = 0.2
self.backgroundColor = .white
let shadowAmount: CGFloat = 2
let rect = CGRect(x: 0, y: 2, width: self.bounds.width + shadowAmount * 0.1, height: self.bounds.height + shadowAmount * 0.1)
self.layer.shadowPath = UIBezierPath(rect: rect).cgPath
}
func createImage(systemName: String) {
let image = UIImage(systemName: systemName)!
// let renderer = UIGraphicsImageRenderer(bounds: self.frame)
// let renderedImage = renderer.image { (_) in
// image.draw(in: frame.insetBy(dx: 16, dy: 30))
// }
// self.image = renderedImage.withRenderingMode(.alwaysTemplate)
self.image = image
}
}
And used it like this:
let size = CGSize(width: 10, height: 10)
let frame = CGRect(origin: CGPoint(x: 360, y: 45), size: size)
let infoView = CustomView(frame: frame, corners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], systemName: "info.circle")
let locationView = CustomView(frame: frame, corners: [], systemName: "location")
let plusView = CustomView(frame: frame, corners: [.layerMinXMaxYCorner, .layerMaxXMaxYCorner], systemName: "plus.circle")
let binocularsView = CustomView(frame: frame, corners: [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner], systemName: "binoculars.fill")
let stackView = UIStackView(arrangedSubviews: [infoView, locationView, plusView, binocularsView])
stackView.frame = CGRect(origin: CGPoint(x: 360, y: 45), size: CGSize(width: 45, height: 180))
stackView.distribution = .fillEqually
stackView.axis = .vertical
stackView.setCustomSpacing(3, after: plusView)
view.addSubview(stackView)
With this code, it looks like this:
As you can see, the icons are too big... So this is what I tried to shrink them: (uncommented the lines in createImage())
func createImage(systemName: String) {
let image = UIImage(systemName: systemName)!
let renderer = UIGraphicsImageRenderer(bounds: self.frame)
let renderedImage = renderer.image { (_) in
image.draw(in: frame.insetBy(dx: 16, dy: 30))
}
self.image = renderedImage.withRenderingMode(.alwaysTemplate)
}
But then its distorted...
Does can help me? :)
Thank you!!!!
Rather than subclassing UIImageView subclass UIView and add a UIImageView as a subview. Then you can create as much spacing around the image as you want.
class CustomView: UIView {
lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
init(frame: CGRect, corners: CACornerMask, systemName: String) {
super.init(frame: frame)
self.setUpViews()
// ...
}
func setUpViews() {
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5),
imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5),
imageView.topAnchor.constraint(equalTo: topAnchor, constant: 5),
imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5),
])
}
func createImage(systemName: String) {
let image = UIImage(systemName: systemName)!
self.imageView.image = image
}
}

Views misplaced when setting frames

I am trying to make a view programatically, making a card with an image and some text and overlaying it with some color. I have turned off clip to bounds and added some colors to make it more visual.
So when I set
overlay.frame = self.frame
or
overlay.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
the overlay should cover the entire view but it does not, why is this?
This is what is showing
This is what I want to see, but with each card having a blue layer on top
private var leftImage: UIImageView = {
let i = UIImageView(frame: .zero)
i.contentMode = .scaleAspectFill
i.image = UIImage()
return i
}()
private var topLabel: UILabel = {
let label = UILabel(frame: .zero)
label.isUserInteractionEnabled = false
label.font = UIFont.boldSystemFont(ofSize: 16.0)
label.lineBreakMode = .byWordWrapping
label.adjustsFontSizeToFitWidth = true
label.numberOfLines = 2
return label
}()
var bottomLabel: UILabel = {
let label = UILabel(frame: .zero)
label.isUserInteractionEnabled = false
label.lineBreakMode = .byWordWrapping
label.font = UIFont.systemFont(ofSize: 12.0)
label.numberOfLines = 1
label.text = "60 seconds"
return label
}()
private var stackView: UIStackView = {
let stack = UIStackView(frame: .zero)
stack.axis = .vertical
stack.isUserInteractionEnabled = false
stack.distribution = .fillEqually
stack.layoutMargins = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
stack.isLayoutMarginsRelativeArrangement = true
return stack
}()
private var overlay: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .blue
view.alpha = 0.8
//view.isHidden = true
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
create()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
create()
}
private func create() {
self.translatesAutoresizingMaskIntoConstraints = false
//leftview
leftImage.frame = CGRect(x: 0, y: 0, width: frame.width / 2, height: frame.height)
addSubview(leftImage)
//rightVIew
stackView.frame = CGRect(x: frame.midX, y: 0, width: frame.width / 2, height: frame.height)
clipsToBounds = false
//add views to stack
stackView.addArrangedSubview(topLabel)
stackView.addArrangedSubview(bottomLabel)
addSubview(stackView)
overlay.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
addSubview(overlay)
//card styling
layer.cornerRadius = 10
}
overlay.frame = self.frame
That can never be right, except by accident, because the frame of overlay and the frame of self are in two different coordinate systems. At the very least you want to say
overlay.frame = self.bounds // bounds, not frame
However, that isn't going to work either unless you say it in the right place. The right place is when layout occurs:
override func layoutSubviews() {
super.layoutSubviews()
self.overlay.frame = self.bounds
}
Simply adding that to your existing code should solve the problem.
The issue is that you're setting your overlay's dimensions like so:
overlay.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
before the superview has determined its final width and height. You'll need to do one of the following:
Call create() after the superview class has determined its final size
Continuously set the overlay's frame as the superview's width and height changes (see https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews)
Use AutoLayout constraints via a xib/storyboard file

How to animate shadow with gradient?

I have a chart like below:
I have a problem with animation and shadows.
i draw a gradient with animation but from the beginning i have the shadow and mask layer which i don't want, i want the shadow animating with the gradient.
the current chart with animation is like below.
i dont want the user see the shadow and mask layer from the beginning.
here is my code:
import Foundation
import UIKit
#IBDesignable class CircularProgressView: UIView {
#IBInspectable var containerCircleColor: UIColor = UIColor.lightGray
#IBInspectable var gradientStartColor: UIColor = UIColor.green
#IBInspectable var gradientEndColor: UIColor = UIColor.yellow
#IBInspectable var arcWidth: CGFloat = 20
override init(frame: CGRect) {
super.init(frame: frame)
circularProgressView_init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
circularProgressView_init()
}
fileprivate func circularProgressView_init() {
let viewHeight = NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0)
self.addConstraint(viewHeight)
}
override func prepareForInterfaceBuilder() {
circularProgressView_init()
}
override func draw(_ rect: CGRect) {
let width = self.bounds.width
let center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
let radius: CGFloat = (width - (arcWidth * 2.5)) / 2
let progressStartAngle: CGFloat = 3 * CGFloat.pi / 2
let progressEndAngle: CGFloat = CGFloat.pi / 2
//fill circular
let circlePath = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: 0,
endAngle: 360,
clockwise: true)
circlePath.lineWidth = arcWidth
containerCircleColor.setStroke()
circlePath.stroke()
//MARK: ProgressPath
let progressPath = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: progressStartAngle,
endAngle: progressEndAngle,
clockwise: true)
progressPath.lineWidth = arcWidth
progressPath.lineCapStyle = .round
//MARK: Gradient
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [gradientStartColor.cgColor , gradientEndColor.cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x:1, y:1)
gradientLayer.frame = self.bounds
//MARK: Animation
let anim = CABasicAnimation(keyPath: "strokeEnd")
anim.duration = 2
anim.fillMode = kCAFillModeForwards
anim.fromValue = 0
anim.toValue = 1
//MARK: Mask Layer
let maskLayer = CAShapeLayer()
maskLayer.path = progressPath.cgPath
maskLayer.fillColor = UIColor.clear.cgColor
maskLayer.strokeColor = UIColor.black.cgColor
maskLayer.lineWidth = arcWidth
maskLayer.lineCap = kCALineCapRound
gradientLayer.mask = maskLayer
self.layer.insertSublayer(gradientLayer, at: 0)
let context = UIGraphicsGetCurrentContext()
let shadow = UIColor.lightGray
let shadowOffset = CGSize(width: 3.1, height: 3.1)
let shadowBlurRadius: CGFloat = 5
context!.saveGState()
context!.setShadow(offset: shadowOffset, blur: shadowBlurRadius, color: (shadow as UIColor).cgColor)
progressPath.stroke()
context?.restoreGState()
maskLayer.add(anim, forKey: nil)
gradientLayer.add(anim, forKey: nil)
}
}
Is it possible at all?
If it is not, how can i at least hide the shadow and mask and show it after animation ends?
You should create another layer to give the shadow.
1 - first you have to create a UIView as a shadowLayer
2 - then mask this shadowLayer with the path and layer same as the maskLayer(in your code) you have already created. e.g: shadowMaskLayer
3 - add the shadow properties to this new shadowMaskLayer
4 - then add the shadowLayer to the original UIView CircularProgressView
5 - also add the animation you already have to this shadowLayer to animate the shadow with the whole circle.
Don't hesitate to ask questions ;)
Well, i put the correct answer here, maybe some one need it in the future.
import Foundation
import UIKit
#IBDesignable class CircularProgressView: UIView {
#IBInspectable var containerCircleColor: UIColor = UIColor.lightGray
#IBInspectable var gradientStartColor: UIColor = UIColor.yellow
#IBInspectable var gradientEndColor: UIColor = UIColor.red
#IBInspectable var arcWidth: CGFloat = 20
#IBInspectable var progressPercent: CGFloat = 50
override init(frame: CGRect) {
super.init(frame: frame)
circularProgressView_init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
circularProgressView_init()
}
fileprivate func circularProgressView_init() {
let viewHeight = NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0)
self.addConstraint(viewHeight)
}
override func prepareForInterfaceBuilder() {
circularProgressView_init()
}
override func draw(_ rect: CGRect) {
let width = self.bounds.width
let center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
let radius: CGFloat = (width - (arcWidth * 2.5)) / 2
let progressStartAngle: CGFloat = 3 * CGFloat.pi / 2
let progressEndAngle: CGFloat = ConvertToTrigonometry.shared.trigonimetryCordinate(percentage: progressPercent) //CGFloat.pi / 2
//fill circular
let circlePath = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: 0,
endAngle: 360,
clockwise: true)
circlePath.lineWidth = arcWidth
containerCircleColor.setStroke()
circlePath.stroke()
//MARK: ProgressPath
let progressPath = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: progressStartAngle,
endAngle: progressEndAngle,
clockwise: true)
progressPath.lineWidth = arcWidth
progressPath.lineCapStyle = .round
//MARK: Gradient
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [gradientStartColor.cgColor , gradientEndColor.cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x:1, y:1)
gradientLayer.frame = self.bounds
//MARK: Mask Layer
let maskLayer = CAShapeLayer()
maskLayer.path = progressPath.cgPath
maskLayer.fillColor = UIColor.clear.cgColor
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.strokeColor = UIColor.black.cgColor
maskLayer.lineWidth = arcWidth
maskLayer.lineCap = kCALineCapRound
maskLayer.masksToBounds = false
gradientLayer.mask = maskLayer
//MARK: Shadow
let shadowLayer = CAShapeLayer()
shadowLayer.frame = bounds
shadowLayer.backgroundColor = UIColor.gray.cgColor
layer.addSublayer(shadowLayer)
let maskShadowLayer = CAShapeLayer()
maskShadowLayer.path = progressPath.cgPath
maskShadowLayer.fillColor = UIColor.clear.cgColor
maskShadowLayer.backgroundColor = UIColor.black.cgColor
maskShadowLayer.strokeColor = UIColor.black.cgColor
maskShadowLayer.lineWidth = arcWidth
maskShadowLayer.lineCap = kCALineCapRound
maskShadowLayer.masksToBounds = false
maskShadowLayer.shadowColor = UIColor.black.cgColor
maskShadowLayer.shadowOpacity = 0.5
maskShadowLayer.shadowOffset = CGSize(width: 3.1, height: 3.1)
shadowLayer.mask = maskShadowLayer
//MARK: Animation
let anim = CABasicAnimation(keyPath: "strokeEnd")
anim.duration = 2
anim.fillMode = kCAFillModeForwards
anim.fromValue = 0
anim.toValue = 1
maskShadowLayer.add(anim, forKey: nil)
maskLayer.add(anim, forKey: nil)
gradientLayer.add(anim, forKey: nil)
layer.addSublayer(gradientLayer)
}
}
And also a helper class which i use for trigonometry conversion:
import Foundation
import UIKit
class ConvertToTrigonometry {
static let shared = ConvertToTrigonometry()
func trigonimetryCordinate(percentage: CGFloat) -> CGFloat {
let pi = CGFloat.pi
let trigonometryRatio = (percentage * 360) / 100 // How much you want to move forward in axis.
let endPointDegree = (3 * pi / 2) + ((trigonometryRatio * 2 / 360) * pi) // End point on axis based on your trigonometryRatio and the start point which is 3pi/2
return endPointDegree
}
}
this solution draw an arc with animating gradient and shadow.
you can find the complete project in my Github.