I am trying to animate and resize a CAGradientLayer using a pan and tap gesture but it's lagging. I am animating other things along with this gradientLayer but speed of animation of gradientLayer slower than that of other UI animation. I have created a sample UIViewController in which I have a rectangle inside which I have my gradient layer. You can simply drag the rectangle to resize it or you can tap on it. You can see the timing difference in animation of rectangle's grey border and gradient layer.
ViewController
class ViewController: UIViewController {
var rectangle: UIView!
var gradientLayer: CAGradientLayer?
var widthConstraint:NSLayoutConstraint!
var shouldExpand = false
override func viewDidLoad() {
super.viewDidLoad()
rectangle = UIView()
view.addSubview(rectangle)
rectangle.translatesAutoresizingMaskIntoConstraints = false
rectangle.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: view.frame.width * 0.05).isActive = true
rectangle.heightAnchor.constraint(equalToConstant: 200).isActive = true
widthConstraint = rectangle.widthAnchor.constraint(equalToConstant: 100)
widthConstraint.isActive = true
rectangle.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
rectangle.layer.cornerRadius = 15
rectangle.layer.borderWidth = 5
rectangle.layer.borderColor = UIColor.lightGray.cgColor
setUpGesture()
setUpGradient()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let layer = gradientLayer {
layer.frame = rectangle.bounds
}
}
func setUpGesture() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture))
rectangle.addGestureRecognizer(panGesture)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
rectangle.addGestureRecognizer(tapGesture)
}
func setUpGradient() {
gradientLayer = CAGradientLayer()
gradientLayer!.frame = rectangle.bounds
gradientLayer!.startPoint = .init(x: -1, y: 0)
gradientLayer!.endPoint = .init(x: 1, y: 0)
gradientLayer!.colors = [UIColor.red.cgColor, UIColor.yellow.cgColor]
gradientLayer!.cornerRadius = 15
rectangle.layer.addSublayer(gradientLayer!)
}
@objc func handlePanGesture(gesture: UIPanGestureRecognizer) {
guard !shouldExpand else { return }
let translation = gesture.translation(in: gesture.view)
switch gesture.state {
case .began, .changed:
if translation.x < -65 || translation.x > (view.frame.width - (100 + (view.frame.width * 0.1)) ) {
gesture.state = .ended
}
widthConstraint.constant = 100 + translation.x
if let layer = gradientLayer {
layer.frame.size.width = 100 + translation.x
}
case .ended, .cancelled:
UIView.animate(withDuration: 0.7, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 5, options: [.curveEaseOut, .allowUserInteraction]) {
self.widthConstraint.constant = 100
if let layer = self.gradientLayer {
layer.frame.size.width = 100
}
self.view.layoutIfNeeded()
}
default:
break
}
}
@objc func handleTapGesture(gesture: UITapGestureRecognizer) {
shouldExpand.toggle()
if shouldExpand {
UIView.animate(withDuration: 0.7, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 5, options: [.curveEaseOut, .allowUserInteraction]) {
self.widthConstraint.constant = self.view.frame.width - (self.view.frame.width * 0.1)
if let layer = self.gradientLayer {
layer.frame.size.width = self.view.frame.width - (self.view.frame.width * 0.1)
}
self.view.layoutIfNeeded()
}
} else {
UIView.animate(withDuration: 0.7, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 5, options: [.curveEaseOut, .allowUserInteraction]) {
self.widthConstraint.constant = 100
if let layer = self.gradientLayer {
layer.frame.size.width = 100
}
self.view.layoutIfNeeded()
}
}
}
}
Alternative approach using SwiftUI
I tried another approach using swiftUI's LinearGeadient. With this approach the drag is smooth, gesture follows rectangle's border as you drag but swiftUI's LinearGeadient does not animate. You can tap on the rectangle and see it not animating. Here's code for other approach. Just remove setUpGradient() function call from viewDidLoad and put setUpSwiftUIGradient function in the view controller and call it in ViewDidLoad.
SwiftUI View
struct SwiftUIGradientView: View {
var uiColors: [UIColor]
var body: some View {
let colors = uiColors.map { Color(uiColor: $0) }
GeometryReader { _ in
LinearGradient(colors: colors, startPoint: .leading, endPoint: .trailing)
.cornerRadius(15)
}
}
}
setUpSwiftUIGradient
func setUpSwiftUIGradient() {
let host = UIHostingController(rootView: SwiftUIGradientView(uiColors: [.red, .yellow]))
let uiView = host.view!
uiView.backgroundColor = .clear
rectangle.addSubview(uiView)
uiView.translatesAutoresizingMaskIntoConstraints = false
uiView.topAnchor.constraint(equalTo: rectangle.topAnchor).isActive = true
uiView.leadingAnchor.constraint(equalTo: rectangle.leadingAnchor).isActive = true
uiView.trailingAnchor.constraint(equalTo: rectangle.trailingAnchor).isActive = true
uiView.bottomAnchor.constraint(equalTo: rectangle.bottomAnchor).isActive = true
}
Can anyone help me get this thing right ?