make the uibutton rounded, shadow and gradient - swift

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)
}
}

Related

SnapKit and custom gradient button Swift

I use SnapKit in my project and trying to add gradient on my button
I have extension:
extension UIButton {
public func setGradientColor(colorOne: UIColor, colorTwo: UIColor) {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
layer.insertSublayer(gradientLayer, at: 0)
}
}
i have button config where i am trying to use gradient:
private var playersButton: UIButton = {
let button = UIButton(type: .custom)
button.setGradientColor(colorOne: .red, colorTwo: .blue)
button.frame = button.layer.frame
return button
}()
and SnapKit here
playersButton.snp.makeConstraints { make in
make.leading.equalToSuperview().inset(60)
make.trailing.equalToSuperview().inset(60)
make.bottom.equalTo(startGameButton).inset(100)
make.height.equalTo(screenHeight/12.82)
}
The problem is i have not result of it, i dont see gradientm but if i delete snapKit config and will use setGradient extension in viewDidLoad it works well!
Example:
private var playersButton: UIButton = {
let button = UIButton(type: .custom)
button.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
playersButton.setGradientColor(colorOne: .blue, colorTwo: .red)
}
How do i change my code to use first method? Thank you
The difference is that when using SnapKit you're using Auto-Layout, so your button's frame is not set when you call button.setGradientColor(colorOne: .red, colorTwo: .blue).
The result is that your gradient layer ends up with a frame size of Zero - so you don't see it.
You will likely find it much easier and more reliable to use a button subclass like this (simple example based on the code you posted):
class MyGradientButton: UIButton {
let gradLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
layer.insertSublayer(gradLayer, at: 0)
}
public func setGradientColor(colorOne: UIColor, colorTwo: UIColor) {
gradLayer.colors = [colorOne.cgColor, colorTwo.cgColor]
gradLayer.locations = [0.0, 1.0]
gradLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
}
override func layoutSubviews() {
super.layoutSubviews()
gradLayer.frame = bounds
}
}
Then change your button declaration to:
private var playersButton: MyGradientButton = {
let button = MyGradientButton()
button.setGradientColor(colorOne: .red, colorTwo: .blue)
return button
}()
Now, whether you set its frame explicitly, or if you use auto-layout (normal syntax or with SnapKit), the gradient layer will automatically adjust whenever the button's frame changes.

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:

how do i fix this error when i try and set the color of a border to a gradient color

I have created a messaging app and I want to set the border colour as a gradient instead of just a solid colour.
So far when I run my code this is what is see:
The gradient colour is supposed to be a border colour for each message cell
I don't know what is making it look the way it does
this is how I coded it :
I've created an extension to deal with the gradient and it looks like this:
extension UIView {
func gradientButton( startColor:UIColor, endColor:UIColor) {
let view:UIView = UIView(frame: self.bounds)
let gradient = CAGradientLayer()
gradient.colors = [startColor.cgColor, endColor.cgColor]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
gradient.frame = self.bounds
self.layer.insertSublayer(gradient, at: 0)
self.mask = view
view.layer.borderWidth = 2
}
}
and when I set the gradient I set it like this in my chatController class
class ChatController: UICollectionViewController, UICollectionViewDelegateFlowLayout, ChatCellSettingsDelegate {
func configureMessage(cell: ChatCell, message: Message) {
cell.bubbleView.gradientButton(startColor: blue, endColor: green)
}
The bubbleView holds the text which is a textView of the message which is created in my ChatCell class:
class ChatCell: UICollectionViewCell {
let bubbleView: UIView = {
let bubble = UIView()
bubble.backgroundColor = UIColor.rgb(red: 0, green: 171, blue: 154, alpha: 1)
bubble.translatesAutoresizingMaskIntoConstraints = false
bubble.layer.masksToBounds = true
bubble.layer.cornerRadius = 13
bubble.backgroundColor = .white
return bubble
}()
let textView: UITextView = {
let text = UITextView()
text.text = "test"
text.font = UIFont.systemFont(ofSize: 16)
text.backgroundColor = .clear
text.translatesAutoresizingMaskIntoConstraints = false
text.isEditable = false
return text
}()
}
how do I fix this?
any help would be helpful
thank you
I can set the border colour to a solid colour with the following line:
cell.bubbleView.layer.borderColor = UIColor.rgb(red: 91, green: 184, blue: 153, alpha: 0.8).cgColor
and then it would look like this:
You can create your own bordered view and use it in your custom cell:
import UIKit
import PlaygroundSupport
public class GradientBorder: UIView {
var startColor: UIColor = .black
var endColor: UIColor = .white
var startLocation: Double = 0.05
var endLocation: Double = 0.95
var path: UIBezierPath!
let shape = CAShapeLayer()
var lineWidth: CGFloat = 5
override public class var layerClass: AnyClass { CAGradientLayer.self }
var gradientLayer: CAGradientLayer { layer as! CAGradientLayer }
func update() {
gradientLayer.startPoint = .init(x: 0.0, y: 0.5)
gradientLayer.endPoint = .init(x: 1.0, y: 0.5)
gradientLayer.locations = [startLocation as NSNumber, endLocation as NSNumber]
gradientLayer.colors = [startColor.cgColor, endColor.cgColor]
path = .init(roundedRect: bounds.insetBy(dx: lineWidth/2, dy: lineWidth/2), byRoundingCorners: [.topLeft, .bottomLeft, .topRight, .bottomRight], cornerRadii: CGSize(width: frame.size.height / 2, height: frame.size.height / 2))
shape.lineWidth = lineWidth
shape.path = path.cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradientLayer.mask = shape
}
override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
update()
}
}
Playground testing:
let gradientBorderedView = GradientBorder()
gradientBorderedView.frame = .init(origin: .zero, size: .init(width: 200, height: 40))
let view = UIView()
view.frame = .init(origin: .zero, size: .init(width: 375, height: 812))
view.backgroundColor = .blue
view.addSubview(gradientBorderedView)
PlaygroundPage.current.liveView = view

Adding multiple shadows hides the background colour and text UIButton

I was trying to add multiple shadows to my UIButton. The two shadows were added as you can see in the image. However, they hide the title and background colour of UIButton. Why is this happening? So, has the order of the layers become bottomLayer, topLayer, text and background?
The actual result
The expected Result
This is how my UIButton class looks.
class PrimaryButton: UIButton {
let cornerRadius: CGFloat = 10
override init(frame: CGRect) {
super.init(frame: frame)
}
convenience init() {
self.init(frame: .zero)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
addDropShadow()
}
private func configure() {
backgroundColor = .white;
layer.cornerRadius = cornerRadius
setTitle("Get Followers", for: .normal)
setTitleColor(Colours.buttonTextColour, for: .normal)
}
private func addDropShadow() {
let topLayer = createShadowLayer(color: Colours.shadowWhite, offset: CGSize(width: -6, height: -6))
let bottomLayer = createShadowLayer(color: Colours.shadowBlack, offset: CGSize(width: 6, height: 6))
layer.addSublayer(topLayer)
layer.addSublayer(bottomLayer)
}
private func createShadowLayer(color: UIColor, offset: CGSize) -> CALayer {
let shadowLayer = CALayer()
shadowLayer.masksToBounds = false
shadowLayer.shadowColor = color.cgColor
shadowLayer.shadowOpacity = 1
shadowLayer.shadowOffset = offset
shadowLayer.shadowRadius = 10
shadowLayer.shouldRasterize = true
shadowLayer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: 10).cgPath
return shadowLayer
}
}
I was able to solve the problem as follows:
Instead of adding the layers with the addSublayer(_ layer: CALayer) method, I used the insertSublayer(_ layer: CALayer, at idx: UInt32) method.
private func addDropShadow() {
let topLayer = createShadowLayer(color: Colours.shadowWhite, offset: CGSize(width: -6, height: -6))
let bottomLayer = createShadowLayer(color: Colours.shadowBlack, offset: CGSize(width: 6, height: 6))
// Modified
layer.insertSublayer(topLayer, at: 0)
layer.insertSublayer(bottomLayer, at: 0)
}
Also, in your createShadowLayer method, instead of CALayer, I returned a CAShapeLayer.
private func createShadowLayer(color: UIColor, offset: CGSize) -> CAShapeLayer {
// Modified
let shadowLayer = CAShapeLayer()
shadowLayer.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.cornerRadius).cgPath
shadowLayer.fillColor = self.backgroundColor?.cgColor
shadowLayer.shadowPath = shadowLayer.path
// shadowLayer.masksToBounds = false
shadowLayer.shadowColor = color.cgColor
shadowLayer.shadowOpacity = 1
shadowLayer.shadowOffset = offset
shadowLayer.shadowRadius = 10
// shadowLayer.shouldRasterize = true
return shadowLayer
}
I don't know if either change is essential. But that's how it worked for me :)
Hello below code will help you, I got exact result what you want in button shadow
just replace some function with my code,
Your code:
override func layoutSubviews() {
super.layoutSubviews()
addDropShadow()
}
Replace it with my code:
override func layoutSubviews() {
super.layoutSubviews()
addDropShadow(color: UIColor.red, offset: CGSize(width: -6, height: -6), btnLayer: self.layer)
addDropShadow(color: UIColor.blue, offset: CGSize(width: 6, height: 6), btnLayer: self.layer)
}
Your code:
private func addDropShadow() {
let topLayer = createShadowLayer(color: Colours.shadowWhite, offset: CGSize(width: -6, height: -6))
let bottomLayer = createShadowLayer(color: Colours.shadowBlack, offset: CGSize(width: 6, height: 6))
layer.addSublayer(topLayer)
layer.addSublayer(bottomLayer)
}
private func createShadowLayer(color: UIColor, offset: CGSize) -> CALayer {
let shadowLayer = CALayer()
shadowLayer.masksToBounds = false
shadowLayer.shadowColor = color.cgColor
shadowLayer.shadowOpacity = 1
shadowLayer.shadowOffset = offset
shadowLayer.shadowRadius = 10
shadowLayer.shouldRasterize = true
shadowLayer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: 10).cgPath
return shadowLayer
}
Replace it with below code:
private func addDropShadow(color: UIColor, offset: CGSize, btnLayer : CALayer)
{
btnLayer.masksToBounds = false
btnLayer.shadowColor = color.cgColor
btnLayer.shadowOpacity = 1
btnLayer.shadowOffset = offset
btnLayer.shadowRadius = 10
}
no need to you private func createShadowLayer(color: UIColor, offset: CGSize) -> CALayer
you can remove that function.
and make sure your button type is custom

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.