Thank you all for your replies. My new fixed circleView now is: import UIKit
class CircleView: UIView {
private let progressLayer: CAShapeLayer = CAShapeLayer()
private let backgroundLayer = CAShapeLayer()
private var progressLabel: UILabel
required init?(coder aDecoder: NSCoder) {
progressLabel = UILabel()
super.init(coder: aDecoder)
createProgressLayer()
// createLabel()
}
override init(frame: CGRect) {
progressLabel = UILabel()
super.init(frame: frame)
createProgressLayer()
// createLabel()
}
// func createLabel() {
// progressLabel = UILabel(frame: CGRectMake(0.0, 0.0, CGRectGetWidth(frame), 60.0))
// progressLabel.textColor = .whiteColor()
// progressLabel.textAlignment = .Center
// progressLabel.text = "Load content"
// progressLabel.font = UIFont(name: "HelveticaNeue-UltraLight", size: 40.0)
// progressLabel.translatesAutoresizingMaskIntoConstraints = false
// addSubview(progressLabel)
//
// addConstraint(NSLayoutConstraint(item: self, attribute: .CenterX, relatedBy: .Equal, toItem: progressLabel, attribute: .CenterX, multiplier: 1.0, constant: 0.0))
// addConstraint(NSLayoutConstraint(item: self, attribute: .CenterY, relatedBy: .Equal, toItem: progressLabel, attribute: .CenterY, multiplier: 1.0, constant: 0.0))
// }
private func createProgressLayer() {
var arcWidth: CGFloat = 3
var arcBgColor: UIColor = UIColor(red:0.94, green:0.94, blue:0.94, alpha:1.0)
let startAngle = CGFloat(M_PI_2)
let endAngle = CGFloat(M_PI * 2 + M_PI_2)
let radius: CGFloat = max(bounds.width, bounds.height)
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let centerPoint = CGPointMake(CGRectGetWidth(frame)/2 , CGRectGetHeight(frame)/2)
let gradientMaskLayer = gradientMask()
progressLayer.path = UIBezierPath(arcCenter:centerPoint, radius: CGRectGetWidth(frame)/2 - 30.0, startAngle:startAngle, endAngle:endAngle, clockwise: true).CGPath
backgroundLayer.path = UIBezierPath(arcCenter: centerPoint, radius: CGRectGetWidth(frame)/2 - 30.0, startAngle: startAngle, endAngle: endAngle, clockwise: true).CGPath
backgroundLayer.fillColor = UIColor.clearColor().CGColor
backgroundLayer.lineWidth = 15.0
backgroundLayer.strokeColor = arcBgColor.CGColor
self.layer.addSublayer(backgroundLayer)
progressLayer.backgroundColor = UIColor.clearColor().CGColor
progressLayer.fillColor = nil
progressLayer.strokeColor = UIColor.blackColor().CGColor
progressLayer.lineWidth = 15.0
progressLayer.strokeStart = 0.0
progressLayer.strokeEnd = 0.0
gradientMaskLayer.mask = progressLayer
layer.addSublayer(gradientMaskLayer)
}
private func gradientMask() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.locations = [0.0, 1.0]
let colorTop: AnyObject = UIColor(red: 255.0/255.0, green: 213.0/255.0, blue: 63.0/255.0, alpha: 1.0).CGColor
let colorBottom: AnyObject = UIColor(red: 255.0/255.0, green: 198.0/255.0, blue: 5.0/255.0, alpha: 1.0).CGColor
let arrayOfColors: [AnyObject] = [colorTop, colorBottom]
gradientLayer.colors = arrayOfColors
return gradientLayer
}
//
// func hideProgressView() {
// progressLayer.strokeEnd = 0.0
// progressLayer.removeAllAnimations()
// progressLabel.text = "Load content"
// }
func animateProgressView() {
// progressLabel.text = "Loading..."
progressLayer.strokeEnd = 0.0
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = CGFloat(0.0)
animation.toValue = CGFloat(0.7)
animation.duration = 1.0
animation.delegate = self
animation.removedOnCompletion = false
animation.additive = true
animation.fillMode = kCAFillModeForwards
progressLayer.addAnimation(animation, forKey: "strokeEnd")
}
}
But I would really like to know now how to:
Do this with the corners, to leave like apple watch one:
enter image description here
and how to move the percentage label, from 0 to 70%, I mean, to animate from 0 to 70 at the same time the circle animates.
The simple way to do this is to use a CAShapeLayer and animate the strokeEnd. Otherwise you'll just have to draw all the intermediate shapes yourself and create your own animation.
Related
I'm trying to create a Shimmer effect to a UIButton, so i've subclassed it and tried to start the animation, but nothing happens. I can see the gradient on the button, but it is not animating. What am I doing wrong?
This is how it looks:
Here is my code:
class ShimmerButton: UIButton {
private var gradientColorOne : CGColor = UIColor(white: 0.85, alpha: 0.0).cgColor
private var gradientColorTwo : CGColor = UIColor(white: 0.95, alpha: 0.5).cgColor
func addGradientLayer() -> CAGradientLayer {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.colors = [self.gradientColorOne, self.gradientColorTwo, self.gradientColorOne]
gradientLayer.locations = [0.0, 0.5, 1.0]
let top = self.layer.sublayers?.last
self.layer.insertSublayer(gradientLayer, above: top)
return gradientLayer
}
func addAnimation(duration: Double, repeatCount: Float) -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.repeatCount = repeatCount
animation.duration = duration
return animation
}
func startAnimating(duration: Double, repeatCount: Float) {
let gradientLayer = self.addGradientLayer()
let animation = self.addAnimation(duration: duration, repeatCount: repeatCount)
gradientLayer.add(animation, forKey: "anim")
}
}
Starting the animation:
self.button.startAnimating(duration: 0.9, repeatCount: .infinity)
Code sample has been taken from here.
Animating the positions property works just fine.
I wrote a playground that creates a simple gradient animation. It looks like this:
I also incorporated the OPs ShimmerButton (With some changes to make it so you can start/stop the shimmer animation. The shimmer animation looks like this:
Here is the code for that playground:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class ShimmerButton: UIButton {
public var animating = false
private var gradientLayer: CAGradientLayer?
private var gradientColorOne : CGColor = UIColor(white: 0.85, alpha: 0.0).cgColor
private var gradientColorTwo : CGColor = UIColor(white: 0.95, alpha: 0.5).cgColor
private func addGradientLayer() -> CAGradientLayer? {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.bounds
gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.colors = [self.gradientColorOne, self.gradientColorTwo, self.gradientColorOne]
gradientLayer.locations = [-1.0, -0.5, 0.0]
let top = self.layer.sublayers?.last
self.layer.insertSublayer(gradientLayer, above: top)
return gradientLayer
}
public func addAnimation(duration: Double, repeatCount: Float) -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-1.0, -0.5, 0.0]
animation.toValue = [1.0, 1.5, 2.0]
animation.repeatCount = repeatCount
animation.duration = duration
animation.delegate = self
return animation
}
public func stopAnimating() {
guard let gradientLayer = gradientLayer else { return }
gradientLayer.removeFromSuperlayer()
gradientLayer.removeAllAnimations()
self.gradientLayer = nil
animating = false
}
public func startAnimating(duration: Double, repeatCount: Float) {
if !animating {
animating = true
gradientLayer = self.addGradientLayer()
let animation = self.addAnimation(duration: duration, repeatCount: repeatCount)
gradientLayer?.add(animation, forKey: "anim")
}
}
}
extension ShimmerButton: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished: Bool) {
animating = false
if finished {
gradientLayer?.removeFromSuperlayer()
gradientLayer = nil;
}
}
}
class GradientView: UIView {
var animationIsLeft: Bool = false
var animating = false
static override var layerClass: AnyClass {
return CAGradientLayer.self
}
public func animateGradient() {
animating = true
animateGradientStep()
}
public func animateGradientStep() {
guard animating,
let layer = layer as? CAGradientLayer else { return }
let animation = CABasicAnimation(keyPath:"locations")
animation.duration = 0.5
animationIsLeft = !animationIsLeft
animation.fromValue = animationIsLeft ? [0, 0.1, 1] : [0, 0.9, 1.0]
let toValue = !animationIsLeft ? [0, 0.1, 1] : [0, 0.9, 1.0]
animation.toValue = toValue
animation.delegate = self
DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) {
layer.locations = toValue.map { NSNumber(value: $0) }
}
layer.add(animation, forKey: nil)
}
func doInitSetup() {
guard let layer = layer as? CAGradientLayer else { return }
layer.startPoint = CGPoint(x: 0.0, y: 1.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
layer.colors = [UIColor.blue.cgColor, UIColor.red.cgColor, UIColor.blue.cgColor]
layer.locations = [0, 0.1, 1]
}
override init(frame: CGRect) {
super.init(frame: frame)
doInitSetup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
doInitSetup()
}
}
extension GradientView: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
animateGradientStep()
}
}
class MyViewController : UIViewController {
var gradientView: GradientView?
var button: ShimmerButton?
override func loadView() {
let view = UIView()
view.backgroundColor = .white
view.layer.borderWidth = 1
self.view = view
gradientView = GradientView()
if let gradientView = gradientView {
gradientView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(gradientView)
gradientView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true
gradientView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 100).isActive = true
gradientView.heightAnchor.constraint(equalToConstant: 200).isActive = true
gradientView.widthAnchor.constraint(equalToConstant: 200).isActive = true
gradientView.layer.borderWidth = 1
}
button = ShimmerButton()
guard let button = button else { return }
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
button.setTitle("Button", for: .normal)
button.backgroundColor = UIColor.lightGray
button.layer.borderWidth = 1
button.layer.cornerRadius = 10
button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true
button.widthAnchor.constraint(equalToConstant: 120).isActive = true
button.heightAnchor.constraint(equalToConstant: 30).isActive = true
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
}
#IBAction func buttonTapped(_ sender: UIButton) {
if gradientView?.animating == true {
gradientView?.animating = false
} else {
gradientView?.animateGradient()
}
guard let button = button else { return }
if button.animating == true {
button.stopAnimating()
} else {
button.startAnimating(duration: 1.0, repeatCount: Float.greatestFiniteMagnitude)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
}
PlaygroundPage.current.liveView = MyViewController()
I made a custom progressbar for my app (following an article on medium), it works as intended but i have one problem, when i change the progress value then it jumps to fast! (dont get confused by the percent values below the bar, they are off, i know that)
i use setNeedsDisplay() to redraw my view.
I want the bar to animate smoothly, so in my case a bit slower.
this is the draw function of the bar:
override func draw(_ rect: CGRect) {
backgroundMask.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height * 0.25).cgPath
layer.mask = backgroundMask
let progressRect = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
progressLayer.frame = progressRect
progressLayer.backgroundColor = UIColor.black.cgColor
gradientLayer.frame = rect
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
gradientLayer.endPoint = CGPoint(x: progress, y: 0.5)
}
Here is the whole Class i used:
https://bitbucket.org/mariwi/custom-animated-progress-bars-with-uibezierpaths/src/master/ProgressBars/Bars/GradientHorizontalProgressBar.swift
Anyone with an idea?
EDIT 1:
Similar questions helped, but the result is not working properly.
I aded this function to set the progress of the bar:
func setProgress(to percent : CGFloat)
{
progress = percent
print(percent)
let rect = self.bounds
let oldBounds = progressLayer.bounds
let newBounds = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
let redrawAnimation = CABasicAnimation(keyPath: "bounds")
redrawAnimation.fromValue = oldBounds
redrawAnimation.toValue = newBounds
redrawAnimation.fillMode = .forwards
redrawAnimation.isRemovedOnCompletion = false
redrawAnimation.duration = 0.5
progressLayer.bounds = newBounds
gradientLayer.endPoint = CGPoint(x: progress, y: 0.5)
progressLayer.add(redrawAnimation, forKey: "redrawAnim")
}
And now the bar behaves like this:
After digging a while and a ton of testing, i came up with a solution, that suited my needs! Altough the above answer from DonMag was also working great (thanks for your effort), i wanted to fix what halfway worked. So the problem was, that the bar resized itself from the middle of the view. And on top, the position was also off for some reason.
First i set the position back to (0,0) so that the view started at the beginning (where it should).
The next thing was the resizing from the middle, because with the position set back, the bar only animated to the half when i set it to 100%. After some tinkering and reading i found out, that changing the anchorPoint of the view would solve my problem. The default value was (0.5,0.5), changing it into (0,0) meant that it would only expand the desired direction.
After that i only needed to re-set the end of the gradient, so that the animation stays consistent between the different values. After all of this my bar worked like I imagined. And here is the result:
Here is the final code, i used to accomplish this:
func setProgress(to percent : CGFloat)
{
progress = percent
print(percent)
let duration = 0.5
let rect = self.bounds
let oldBounds = progressLayer.bounds
let newBounds = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
let redrawAnimation = CABasicAnimation(keyPath: "bounds")
redrawAnimation.fromValue = oldBounds
redrawAnimation.toValue = newBounds
redrawAnimation.fillMode = .both
redrawAnimation.isRemovedOnCompletion = false
redrawAnimation.duration = duration
progressLayer.bounds = newBounds
progressLayer.position = CGPoint(x: 0, y: 0)
progressLayer.anchorPoint = CGPoint(x: 0, y: 0)
progressLayer.add(redrawAnimation, forKey: "redrawAnim")
let oldGradEnd = gradientLayer.endPoint
let newGradEnd = CGPoint(x: progress, y: 0.5)
let gradientEndAnimation = CABasicAnimation(keyPath: "endPoint")
gradientEndAnimation.fromValue = oldGradEnd
gradientEndAnimation.toValue = newGradEnd
gradientEndAnimation.fillMode = .both
gradientEndAnimation.isRemovedOnCompletion = false
gradientEndAnimation.duration = duration
gradientLayer.endPoint = newGradEnd
gradientLayer.add(gradientEndAnimation, forKey: "gradEndAnim")
}
I'm going to suggest a somewhat different approach.
First, instead of adding a sublayer as the gradient layer, we'll make the custom view's layer itself a gradient layer:
private var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
// then, in init
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
We'll set the gradient animation to the full size of the view... that will give it a consistent width and speed.
Next, we'll add a subview as a mask, instead of a layer-mask. That will allow us to animate its width independently.
class GradProgressView: UIView {
#IBInspectable var color: UIColor = .gray {
didSet { setNeedsDisplay() }
}
#IBInspectable var gradientColor: UIColor = .white {
didSet { setNeedsDisplay() }
}
// this view will mask the percentage width
private let myMaskView = UIView()
// so we can calculate the new-progress-animation duration
private var curProgress: CGFloat = 0.0
public var progress: CGFloat = 0 {
didSet {
// calculate the change in progress
let changePercent = abs(curProgress - progress)
// if the change is 100% (i.e. from 0.0 to 1.0),
// we want the animation to take 1-second
// so, make the animation duration equal to
// 1-second * changePercent
let dur = changePercent * 1.0
// save the new progress
curProgress = progress
// calculate the new width of the mask view
var r = bounds
r.size.width *= progress
// animate the size of the mask-view
UIView.animate(withDuration: TimeInterval(dur), animations: {
self.myMaskView.frame = r
})
}
}
private var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
gradientLayer.locations = [0.25, 0.5, 0.75]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-0.3, -0.15, 0]
animation.toValue = [1, 1.15, 1.3]
animation.duration = 1.5
animation.isRemovedOnCompletion = false
animation.repeatCount = Float.infinity
gradientLayer.add(animation, forKey: nil)
myMaskView.backgroundColor = .white
mask = myMaskView
}
override func layoutSubviews() {
super.layoutSubviews()
// if the mask view frame has not been set at all yet
if myMaskView.frame.height == 0 {
var r = bounds
r.size.width = 0.0
myMaskView.frame = r
}
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
layer.cornerRadius = bounds.height * 0.25
}
}
Here's a sample controller class - each tap will cycle through a list of sample progress percentages:
class ExampleViewController: UIViewController {
let progView = GradProgressView()
let infoLabel = UILabel()
var idx: Int = 0
let testVals: [CGFloat] = [
0.75, 0.3, 0.95, 0.25, 0.5, 1.0,
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
[infoLabel, progView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
infoLabel.textColor = .white
infoLabel.textAlignment = .center
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
progView.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
progView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
progView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
progView.heightAnchor.constraint(equalToConstant: 40.0),
infoLabel.topAnchor.constraint(equalTo: progView.bottomAnchor, constant: 8.0),
infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
])
progView.color = #colorLiteral(red: 0.9932278991, green: 0.5762576461, blue: 0.03188031539, alpha: 1)
progView.gradientColor = #colorLiteral(red: 1, green: 0.8578521609, blue: 0.3033572137, alpha: 1)
// add a tap gesture recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTap(_:)))
view.addGestureRecognizer(t)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
didTap(nil)
}
#objc func didTap(_ g: UITapGestureRecognizer?) -> Void {
let n = idx % testVals.count
progView.progress = testVals[n]
idx += 1
infoLabel.text = "Auslastung \(Int(testVals[n] * 100))%"
}
}
How to add a shadow effect to this gradient border.
Here is the sample extension to create a border layer with a specified width. When I tried to add a shadow layer whole UI gets affected.
self.gradientBorder(width: 3, colors: UIColor.defaultGradient, andRoundCornersWithRadius: min(bounds.size.height, bounds.size.width))
extension UIView {
private static let kLayerNameGradientBorder = "GradientBorderLayer"
func gradientBorder(width: CGFloat,
colors: [UIColor],
startPoint: CGPoint = CGPoint(x: 1.0, y: 0.0),
endPoint: CGPoint = CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius cornerRadius: CGFloat = 0) {
let existingBorder = gradientBorderLayer()
let border = existingBorder ?? CAGradientLayer()
border.frame = CGRect(x: bounds.origin.x, y: bounds.origin.y,
width: bounds.size.width + width, height: bounds.size.height + width)
border.colors = colors.map { $0.cgColor }
border.startPoint = startPoint
border.endPoint = endPoint
let mask = CAShapeLayer()
let maskRect = CGRect(x: bounds.origin.x + width/2, y: bounds.origin.y + width/2,
width: bounds.size.width - width, height: bounds.size.height - width)
let path = UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath
mask.path = path
mask.fillColor = UIColor.clear.cgColor
mask.strokeColor = UIColor.black.cgColor
mask.backgroundColor = UIColor.black.cgColor
mask.lineWidth = width
mask.masksToBounds = false
border.mask = mask
let exists = (existingBorder != nil)
if !exists {
layer.addSublayer(border)
}
}
private func gradientBorderLayer() -> CAGradientLayer? {
let borderLayers = layer.sublayers?.filter { return $0.name == UIView.kLayerNameGradientBorder }
if borderLayers?.count ?? 0 > 1 {
fatalError()
}
return borderLayers?.first as? CAGradientLayer
}
}
Edit
Minor changes from initial code:
background layer doesn't interfere with added subviews
handles resizing correctly (when called in viewDidLayoutSubviews)
You can do this by adding a shadow properties to the view's layer, and adding another layer as a "background" layer.
After Edit... Here is your UIView extension - slightly modified (see the comments):
extension UIView {
private static let kLayerNameGradientBorder = "GradientBorderLayer"
private static let kLayerNameBackgroundLayer = "BackgroundLayer"
func gradientBorder(width: CGFloat,
colors: [UIColor],
startPoint: CGPoint = CGPoint(x: 1.0, y: 0.0),
endPoint: CGPoint = CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius cornerRadius: CGFloat = 0,
bgColor: UIColor = .white,
shadowColor: UIColor = .black,
shadowRadius: CGFloat = 5.0,
shadowOpacity: Float = 0.75,
shadowOffset: CGSize = CGSize(width: 0.0, height: 0.0)
) {
let existingBackground = backgroundLayer()
let bgLayer = existingBackground ?? CALayer()
bgLayer.name = UIView.kLayerNameBackgroundLayer
// set its color
bgLayer.backgroundColor = bgColor.cgColor
// insert at 0 to not cover other layers
if existingBackground == nil {
layer.insertSublayer(bgLayer, at: 0)
}
// use same cornerRadius as border
bgLayer.cornerRadius = cornerRadius
// inset its frame by 1/2 the border width
bgLayer.frame = bounds.insetBy(dx: width * 0.5, dy: width * 0.5)
// set shadow properties
layer.shadowColor = shadowColor.cgColor
layer.shadowRadius = shadowRadius
layer.shadowOpacity = shadowOpacity
layer.shadowOffset = shadowOffset
let existingBorder = gradientBorderLayer()
let border = existingBorder ?? CAGradientLayer()
border.name = UIView.kLayerNameGradientBorder
// don't do this
// border.frame = CGRect(x: bounds.origin.x, y: bounds.origin.y,
// width: bounds.size.width + width, height: bounds.size.height + width)
// use this instead
border.frame = bounds
border.colors = colors.map { $0.cgColor }
border.startPoint = startPoint
border.endPoint = endPoint
let mask = CAShapeLayer()
let maskRect = CGRect(x: bounds.origin.x + width/2, y: bounds.origin.y + width/2,
width: bounds.size.width - width, height: bounds.size.height - width)
let path = UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath
mask.path = path
mask.fillColor = UIColor.clear.cgColor
mask.strokeColor = UIColor.black.cgColor
mask.backgroundColor = UIColor.black.cgColor
mask.lineWidth = width
mask.masksToBounds = false
border.mask = mask
let exists = (existingBorder != nil)
if !exists {
layer.addSublayer(border)
}
}
private func backgroundLayer() -> CALayer? {
let aLayers = layer.sublayers?.filter { return $0.name == UIView.kLayerNameBackgroundLayer }
if aLayers?.count ?? 0 > 1 {
fatalError()
}
return aLayers?.first
}
private func gradientBorderLayer() -> CAGradientLayer? {
let borderLayers = layer.sublayers?.filter { return $0.name == UIView.kLayerNameGradientBorder }
if borderLayers?.count ?? 0 > 1 {
fatalError()
}
return borderLayers?.first as? CAGradientLayer
}
}
After Edit... and here's an example in-use:
class GradBorderViewController: UIViewController {
var topGradView: UIView = UIView()
// make bottom grad view a button
var botGradView: UIButton = UIButton()
var topBkgView: UIView = UIView()
var botBkgView: UIView = UIView()
let embededLabel: UILabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
embededLabel.textColor = .red
embededLabel.textAlignment = .center
embededLabel.text = "Label as subview"
botGradView.setTitle("Button", for: [])
botGradView.setTitleColor(.red, for: [])
botGradView.setTitleColor(.lightGray, for: .highlighted)
topGradView.backgroundColor = .clear
botGradView.backgroundColor = .clear
topBkgView.backgroundColor = .yellow
botBkgView.backgroundColor = UIColor(red: 0.5, green: 0.0, blue: 0.0, alpha: 1.0)
[topBkgView, topGradView, botBkgView, botGradView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
embededLabel.translatesAutoresizingMaskIntoConstraints = false
// embed label in topGradView
topGradView.addSubview(embededLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// yellow background view on top half, dark-red background view on bottom half
topBkgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
topBkgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
botBkgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
botBkgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
topBkgView.topAnchor.constraint(equalTo: g.topAnchor),
botBkgView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
topBkgView.heightAnchor.constraint(equalTo: g.heightAnchor, multiplier: 0.5),
botBkgView.topAnchor.constraint(equalTo: topBkgView.bottomAnchor),
// each grad border view 75% of width, 80-pt constant height
topGradView.widthAnchor.constraint(equalTo: topBkgView.widthAnchor, multiplier: 0.75),
topGradView.heightAnchor.constraint(equalToConstant: 80.0),
botGradView.widthAnchor.constraint(equalTo: topGradView.widthAnchor),
botGradView.heightAnchor.constraint(equalTo: topGradView.heightAnchor),
// center each grad border view in a background view
topGradView.centerXAnchor.constraint(equalTo: topBkgView.centerXAnchor),
topGradView.centerYAnchor.constraint(equalTo: topBkgView.centerYAnchor),
botGradView.centerXAnchor.constraint(equalTo: botBkgView.centerXAnchor),
botGradView.centerYAnchor.constraint(equalTo: botBkgView.centerYAnchor),
// center the embedded label in the topGradView
embededLabel.centerXAnchor.constraint(equalTo: topGradView.centerXAnchor),
embededLabel.centerYAnchor.constraint(equalTo: topGradView.centerYAnchor),
])
botGradView.addTarget(self, action: #selector(self.testTap(_:)), for: .touchUpInside)
}
#objc func testTap(_ sender: Any?) -> Void {
print("Tapped!")
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let a1: [CGFloat] = [173, 97, 222].map({$0 / 255.0})
let a2: [CGFloat] = [0, 198, 182].map({$0 / 255.0})
let c1 = UIColor(red: a1[0], green: a1[1], blue: a1[2], alpha: 1.0)
let c2 = UIColor(red: a2[0], green: a2[1], blue: a2[2], alpha: 1.0)
topGradView.gradientBorder(width: 6,
colors: [c1, c2],
startPoint: CGPoint(x: 0.0, y: 0.0),
endPoint: CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius: topGradView.frame.height * 0.5
)
botGradView.gradientBorder(width: 6,
colors: [c1, c2],
startPoint: CGPoint(x: 0.0, y: 0.0),
endPoint: CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius: topGradView.frame.height * 0.5,
shadowColor: .white,
shadowRadius: 12,
shadowOpacity: 0.95,
shadowOffset: CGSize(width: 0.0, height: 0.0)
)
}
}
After Edit... Results:
I have a UIView which I am making the background a circular shape with:
self.colourView.layer.cornerRadius = 350
self.colourView.clipsToBounds = true
With the view being a 700 by 700 square.
I am then trying to change the size of this view using:
UIView.animate(withDuration: zoomLength,
delay: 0,
options: .curveEaseIn,
animations: {
self.colorViewWidth.constant = 100
self.colorViewHeight.constant = 100
self.colourView.layer.cornerRadius = 50
self.colourView.clipsToBounds = true
self.view.layoutIfNeeded()
},
completion: { finished in
})
})
The problem with this is the corner radius of the view switch to 50 instantly. So that as the view shrinks in size it doesn't look like a circle shrinking but a square with rounded corners until it reaches the 100 by 100 size.
How can I take a view that circule in shape and shrink it down while maintaining the circlare shape during the animation.
You could always animate the corner radius as well with a CABasicAnimation along side the UIView animations. See example below.
import UIKit
class ViewController: UIViewController {
//cause 'merica
var colorView = UIView()
var button = UIButton()
var colorViewWidth : NSLayoutConstraint!
var colorViewHeight : NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
colorView.frame = CGRect(x: 0, y: 0, width: 750, height: 750)
colorView.center = self.view.center
colorView.backgroundColor = .blue
colorView.layer.cornerRadius = 350
colorView.clipsToBounds = true
colorView.layer.allowsEdgeAntialiasing = true
colorView.translatesAutoresizingMaskIntoConstraints = false
//set up constraints
colorViewWidth = NSLayoutConstraint(item: colorView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 750)
colorViewWidth.isActive = true
colorViewHeight = NSLayoutConstraint(item: colorView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 750)
colorViewHeight.isActive = true
self.view.addSubview(colorView)
colorView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
colorView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
//button for action
button = UIButton(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width - 20, height: 50))
button.center = self.view.center
button.center.y = self.view.bounds.height - 70
button.addTarget(self, action: #selector(ViewController.pressed(sender:)), for: .touchUpInside)
button.setTitle("Animate", for: .normal)
button.setTitleColor(.blue, for: .normal)
self.view.addSubview(button)
}
func pressed(sender:UIButton) {
self.colorViewWidth.constant = 100
self.colorViewHeight.constant = 100
UIView.animate(withDuration: 1.0,
delay: 0,
options: .curveEaseIn,
animations: {
self.view.layoutIfNeeded()
},
completion: { finished in
})
//animate cornerRadius and update model
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.duration = 1.0
//super important must match
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
animation.fromValue = colorView.layer.cornerRadius
animation.toValue = 50
colorView.layer.add(animation, forKey: nil)
//update model important
colorView.layer.cornerRadius = 50
}
}
Result:
Try to put your image inside a viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
colourView.layer.cornerRadius = colourView.frame.size.width/2
colourView.layer.masksToBounds = true
colourView.layer.borderWidth = 3.0
colourView.layer.borderColor = UIColor(red: 142.0/255.5, green: 142.0/255.5, blue: 142.0/255.5, alpha: 1.0).cgColor
}
let me know how it goes
Cheers
Regarding animation you need to create the CAShapeLayer object and then set the cgpath of the layer object.
let circleLayer = CAShapeLayer()
let radius = view.bounds.width * 0.5
//Value of startAngle and endAngle should be in the radian.
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
circleLayer.path = path.cgPath
The above will draw the circle for you. After that you can apply the animation on layer object.
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = fromValue//you can update the value here by default it will be 0
animation.toValue = toValue //you can update the value here by default it will be 1
animation.duration = CFTimeInterval(remainingTime)
circleLayer
circleLayer.removeAnimation(forKey: "stroke")
circleLayer.add(animation, forKey: "stroke")
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 6 years ago.
Improve this question
I use a custom indicator but when i call the subclass indicator in my viewdidload my view controller is blank but when i run it in a playground i can see it in the side window. Here is the code of the indicator. Theres no error but my indicator is not showing. Thats my problem. I would appreciate it if someone could tell me why.
import UIKit
class MaterialLoadingIndicator: UIView {
let MinStrokeLength: CGFloat = 0.05
let MaxStrokeLength: CGFloat = 0.7
let circleShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
initShapeLayer()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func initShapeLayer() {
circleShapeLayer.actions = ["strokeEnd" : NSNull(),
"strokeStart" : NSNull(),
"transform" : NSNull(),
"strokeColor" : NSNull()]
circleShapeLayer.backgroundColor = UIColor.clear.cgColor
circleShapeLayer.strokeColor = UIColor.blue.cgColor
circleShapeLayer.fillColor = UIColor.clear.cgColor
circleShapeLayer.lineWidth = 5
circleShapeLayer.lineCap = kCALineCapRound
circleShapeLayer.strokeStart = 0
circleShapeLayer.strokeEnd = MinStrokeLength
let center = CGPoint(x: bounds.width*0.5, y: bounds.height*0.5)
circleShapeLayer.frame = bounds
circleShapeLayer.path = UIBezierPath(arcCenter: center,
radius: center.x,
startAngle: 0,
endAngle: CGFloat(M_PI*2),
clockwise: true).cgPath
layer.addSublayer(circleShapeLayer)
}
func startAnimating() {
if layer.animation(forKey: "rotation") == nil {
startColorAnimation()
startStrokeAnimation()
startRotatingAnimation()
}
}
private func startColorAnimation() {
let color = CAKeyframeAnimation(keyPath: "strokeColor")
color.duration = 10.0
color.values = [UIColor(hex: 0x4285F4, alpha: 1.0).cgColor,
UIColor(hex: 0xDE3E35, alpha: 1.0).cgColor,
UIColor(hex: 0xF7C223, alpha: 1.0).cgColor,
UIColor(hex: 0x1B9A59, alpha: 1.0).cgColor,
UIColor(hex: 0x4285F4, alpha: 1.0).cgColor]
color.calculationMode = kCAAnimationPaced
color.repeatCount = Float.infinity
circleShapeLayer.add(color, forKey: "color")
}
private func startRotatingAnimation() {
let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.toValue = M_PI*2
rotation.duration = 2.2
rotation.isCumulative = true
rotation.isAdditive = true
rotation.repeatCount = Float.infinity
layer.add(rotation, forKey: "rotation")
}
private func startStrokeAnimation() {
let easeInOutSineTimingFunc = CAMediaTimingFunction(controlPoints: 0.39, 0.575, 0.565, 1.0)
let progress: CGFloat = MaxStrokeLength
let endFromValue: CGFloat = circleShapeLayer.strokeEnd
let endToValue: CGFloat = endFromValue + progress
let strokeEnd = CABasicAnimation(keyPath: "strokeEnd")
strokeEnd.fromValue = endFromValue
strokeEnd.toValue = endToValue
strokeEnd.duration = 0.5
strokeEnd.fillMode = kCAFillModeForwards
strokeEnd.timingFunction = easeInOutSineTimingFunc
strokeEnd.beginTime = 0.1
strokeEnd.isRemovedOnCompletion = false
let startFromValue: CGFloat = circleShapeLayer.strokeStart
let startToValue: CGFloat = fabs(endToValue - MinStrokeLength)
let strokeStart = CABasicAnimation(keyPath: "strokeStart")
strokeStart.fromValue = startFromValue
strokeStart.toValue = startToValue
strokeStart.duration = 0.4
strokeStart.fillMode = kCAFillModeForwards
strokeStart.timingFunction = easeInOutSineTimingFunc
strokeStart.beginTime = strokeEnd.beginTime + strokeEnd.duration + 0.2
strokeStart.isRemovedOnCompletion = false
let pathAnim = CAAnimationGroup()
pathAnim.animations = [strokeEnd, strokeStart]
pathAnim.duration = strokeStart.beginTime + strokeStart.duration
pathAnim.fillMode = kCAFillModeForwards
pathAnim.isRemovedOnCompletion = false
CATransaction.begin()
CATransaction.setCompletionBlock {
if self.circleShapeLayer.animation(forKey: "stroke") != nil {
self.circleShapeLayer.transform = CATransform3DRotate(self.circleShapeLayer.transform, CGFloat(M_PI*2) * progress, 0, 0, 1)
self.circleShapeLayer.removeAnimation(forKey: "stroke")
self.startStrokeAnimation()
}
}
circleShapeLayer.add(pathAnim, forKey: "stroke")
CATransaction.commit()
}
func stopAnimating() {
circleShapeLayer.removeAllAnimations()
layer.removeAllAnimations()
circleShapeLayer.transform = CATransform3DIdentity
layer.transform = CATransform3DIdentity
}
}
extension UIColor {
convenience init(hex: UInt, alpha: CGFloat) {
self.init(
red: CGFloat((hex & 0xFF0000) >> 16) / 255.0,
green: CGFloat((hex & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(hex & 0x0000FF) / 255.0,
alpha: CGFloat(alpha)
)
}
}
And here is the code of my view controller in the viewdidload
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 568))
let indicator = MaterialLoadingIndicator(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
indicator.center = CGPoint(x: 320*0.5, y: 568*0.5)
view.addSubview(indicator)
indicator.startAnimating()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
The view holding the indicator is just floating around, feeling lost, feeling unhappy for not belonging to, not being added to someone. :)
EDIT :
Okay John, now that we are stuck, let us add the indicator to someone.
override func viewDidLoad() {
super.viewDidLoad()
let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 568))
let indicator = MaterialLoadingIndicator(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
indicator.center = CGPoint(x: 320*0.5, y: 568*0.5)
view.addSubview(indicator)
indicator.startAnimating()
self.view.addSubview(view) // John, this is what was missing
}