So I'm trying to create a loading indicator on an element, basically it's a path that's expanding and contracting towards the end.
The hard part is that the path is a gradient color which I have no idea to while also animating the paths strokeStart and strokeEnd
Here is what I have so far without the gradient.
//
// Created by Tommy Sadiq Hinrichsen on 07/07/2021.
// Copyright (c) 2021 Dagrofa. All rights reserved.
//
import Foundation
import UIKit
@IBDesignable
class LoadingBorderView: UIView {
private let strokeLayer = CAShapeLayer()
var startPosition: UIRectEdge = .right {
didSet { self.setNeedsDisplay() }
}
private var cornerRadius: CGFloat { return self.frame.height / 2 }
override init(frame: CGRect) {
super.init(frame: frame)
self.configureView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.configureView()
}
private func configureView() {
self.strokeLayer.fillColor = nil
self.strokeLayer.strokeColor = UIColor.black.cgColor
self.strokeLayer.strokeEnd = 0
self.strokeLayer.lineCap = .round
self.layer.addSublayer(self.strokeLayer)
}
private func getRoundedPath() -> UIBezierPath {
let path = UIBezierPath()
let drawTop: (UIBezierPath) -> Void = { (path: UIBezierPath) in
path.move(to: CGPoint(x: 0, y: self.cornerRadius))
path.addArc(withCenter: CGPoint(x: self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 180.cgfloat.toRadians(), endAngle: 270.cgfloat.toRadians(), clockwise: true)
path.addLine(to: CGPoint(x: self.bounds.width - self.cornerRadius, y: 0))
path.addArc(withCenter: CGPoint(x: self.frame.width - self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: -90.cgfloat.toRadians(), endAngle: 0.cgfloat.toRadians(), clockwise: true)
}
let drawBottom: (UIBezierPath) -> Void = { (path: UIBezierPath) in
path.move(to: CGPoint(x: self.frame.width, y: self.cornerRadius))
path.addArc(withCenter: CGPoint(x: self.frame.width - self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 0.cgfloat.toRadians(), endAngle: 90.cgfloat.toRadians(), clockwise: true)
path.addLine(to: CGPoint(x: self.cornerRadius, y: self.frame.height))
path.addArc(withCenter: CGPoint(x: self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: 90.cgfloat.toRadians(), endAngle: 180.cgfloat.toRadians(), clockwise: true)
}
switch self.startPosition {
case .right:
drawBottom(path)
drawTop(path)
case .left:
drawTop(path)
drawBottom(path)
default:
fatalError("LoadingBorderView must start from left or right")
}
return path
}
func startAnimation(duration: TimeInterval = 2.0, delay: TimeInterval = 0.3) {
let strokeStartAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeStart))
strokeStartAnimation.beginTime = delay
strokeStartAnimation.duration = duration
strokeStartAnimation.fromValue = 0
strokeStartAnimation.toValue = 1
let strokeEndAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))
strokeEndAnimation.duration = duration
strokeEndAnimation.fromValue = 0
strokeEndAnimation.toValue = 1
strokeEndAnimation.fillMode = .forwards
let group = CAAnimationGroup()
group.beginTime = CACurrentMediaTime()
group.timingFunction = .easeInOutCubic
group.duration = duration + delay
group.repeatCount = .infinity
group.animations = [strokeStartAnimation, strokeEndAnimation]
self.strokeLayer.add(group, forKey: "strokeLayer")
}
func stopAnimation() {
self.strokeLayer.removeAllAnimations()
}
override func draw(_ rect: CGRect) {
self.strokeLayer.frame = self.bounds
self.strokeLayer.path = self.getRoundedPath().cgPath
}
}
public extension CAMediaTimingFunction {
///https://easings.net/#easeInOutCubic
static var easeInOutCubic = CAMediaTimingFunction(controlPoints: 0.65, 0, 0.35, 1)
}
