There is a problem that my gradient animation is not working correctly. If you add more colors to the gradientColors array, it works more or less, but there is a flickering effect.
Tried solving the problem via CATransaction, but it did not solve the problem. When I add more than 2 colors to the array, the animation seems to work, but there is a flickering effect
there are more than two colors in the colors array
there are 2 colors in the array
Maybe you have a solution to the problem ? I would be grateful for your help:)
class ViewController: UIViewController {
private enum Color {
static var gradientColors: [UIColor] = [
UIColor(red: 255, green: 255, blue: 255, alpha: 1),
UIColor(red: 255, green: 255, blue: 255, alpha: 0)
]
}
@IBOutlet weak var gradientView: UIView! {
didSet {
gradientView.layer.cornerRadius = 16
}
}
private var timer: Timer?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
animateBorderGradietion()
}
func animateBorderGradietion() {
let shape1 = CAShapeLayer()
shape1.path = UIBezierPath(
roundedRect: gradientView.bounds.insetBy(dx: 1.0, dy: 1.0),
cornerRadius: gradientView.layer.cornerRadius
).cgPath
shape1.lineWidth = 1.0
shape1.strokeColor = UIColor.white.cgColor
shape1.fillColor = UIColor.clear.cgColor
let gradient1 = CAGradientLayer()
gradient1.frame = gradientView.bounds
gradient1.type = .conic
gradient1.colors = Color.gradientColors.map { $0.cgColor }
gradient1.locations = calculateGradientLocation()
gradient1.startPoint = CGPoint(x: 0.5, y: 0.5)
gradient1.endPoint = CGPoint(x: 1, y: 1)
gradient1.mask = shape1
gradientView.layer.addSublayer(gradient1)
let shape2 = CAShapeLayer()
shape2.path = UIBezierPath(
roundedRect: gradientView.bounds.insetBy(dx: 1.0, dy: 1.0),
cornerRadius: gradientView.layer.cornerRadius
).cgPath
shape2.lineWidth = 1.0
shape2.strokeColor = UIColor.white.cgColor
shape2.fillColor = UIColor.clear.cgColor
let gradient2 = CAGradientLayer()
gradient2.frame = gradientView.bounds
gradient2.type = .conic
gradient2.colors = Color.gradientColors.map { $0.cgColor }
gradient2.locations = calculateGradientLocation()
gradient2.startPoint = CGPoint(x: 1, y: 1)
gradient2.endPoint = CGPoint(x: 0.5, y: 0.5)
gradient2.mask = shape2
gradientView.layer.addSublayer(gradient2)
self.timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { _ in
CATransaction.begin()
CATransaction.setAnimationDuration(0.2)
CATransaction.setCompletionBlock {
gradient1.removeAnimation(forKey: "myAnimation")
gradient2.removeAnimation(forKey: "myAnimation")
let previous = Color.gradientColors.map { $0.cgColor }
let last = Color.gradientColors.removeLast()
Color.gradientColors.insert(last, at: 0)
let lastColors = Color.gradientColors.map { $0.cgColor }
let colorsAnimation = CABasicAnimation(keyPath: "colors")
colorsAnimation.fromValue = previous
colorsAnimation.toValue = lastColors
colorsAnimation.repeatCount = 1
colorsAnimation.duration = 0.2
colorsAnimation.isRemovedOnCompletion = false
colorsAnimation.fillMode = .both
gradient1.colors = lastColors
gradient2.colors = lastColors
gradient1.add(colorsAnimation, forKey: "myAnimation")
gradient2.add(colorsAnimation, forKey: "myAnimation")
}
CATransaction.commit()
}
}
private func calculateGradientLocation() -> [NSNumber] {
return Array(stride(from: 0, to: Color.gradientColors.count, by: 1))
.map { NSNumber(value: Double($0) / Double(Color.gradientColors.count)) }
}
}
Instead of trying to animate the gradient colors and locations, another approach is to rotate the CAGradientLayer
.
So, if we start with a plain view:
we can add a CAGradientLayer
as a sublayer -- here, it has only a single 1/2 alpha color so we can easily see the framing:
Because we will be rotating that layer, we set that layer's frame to be larger than the view so it will completely cover it:
Next, we set the gradient layer's colors to clear, white, clear
:
and finally, apply a shape layer mask to the view:
Here's how it looks on a black background:
And here are some animated versions (too big to embed here): https://imgur.com/a/JXgyT7b
Here is some example code for that:
// couple helpers for CGRect
extension CGRect {
public var center: CGPoint { return CGPoint(x: midX, y: midY) }
public var diagonalExtent: CGFloat { return hypot(width, height) }
}
// example view
class AnimatedGradientBorderedView: UIView {
public var lineWidth: CGFloat = 1.0 { didSet { mskLayer.lineWidth = lineWidth } }
public var cornerRadius: CGFloat = 16.0 { didSet { setNeedsLayout() } }
private let gradLayer = CAGradientLayer()
private let mskLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
commonInit()
}
func commonInit() {
gradLayer.colors = [UIColor.clear.cgColor, UIColor.white.cgColor, UIColor.clear.cgColor]
layer.addSublayer(gradLayer)
mskLayer.fillColor = UIColor.clear.cgColor
// any opaque color
mskLayer.strokeColor = UIColor.black.cgColor
mskLayer.lineWidth = lineWidth
}
override func layoutSubviews() {
super.layoutSubviews()
// we need the gradient layer frame to cover the entire view
// when it rotates, so we make it a square with width/height
// equal to the diagonal dimension of self (plus just a little "padding")
let diag: CGFloat = bounds.diagonalExtent + 2.0
gradLayer.frame = bounds.insetBy(dx: -(diag - bounds.width) * 0.5, dy: -(diag - bounds.height) * 0.5)
// rounded-corners mask path, inset by lineWidth so it is completely inside of self
mskLayer.path = UIBezierPath(roundedRect: bounds.insetBy(dx: lineWidth, dy: lineWidth), cornerRadius: cornerRadius).cgPath
layer.mask = mskLayer
doAnim()
}
func doAnim() {
// rotate the mask layer
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(Double.pi * 2)
rotateAnimation.isRemovedOnCompletion = false
// adjust rotation speed as desired
rotateAnimation.duration = 8.0
rotateAnimation.repeatCount=Float.infinity
gradLayer.add(rotateAnimation, forKey: nil)
}
}
// simple example view controller
class MyViewController: UIViewController {
let testView = AnimatedGradientBorderedView()
override func viewDidLoad() {
super.viewDidLoad()
testView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 34.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -34.0),
testView.heightAnchor.constraint(equalToConstant: 492.0),
testView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
view.backgroundColor = .black
testView.backgroundColor = .clear
}
}