5

The shape is filled in correctly to begin with but I can't figure out how to change the fill color or, even better, how to animate a fill color change on a UIBezierPath. Just like changing the background color of a UIView is what I'm looking for.

var fillColor = UIColor()

func changeBackgroundColor() {

    let animcolor = CABasicAnimation(keyPath: "fillColor")
    animcolor.fromValue = UIColor.greenColor()
    animcolor.toValue = UIColor.orangeColor()
    animcolor.duration = 1.0;
    animcolor.repeatCount = 0;
    animcolor.autoreverses = true
    shapeLayer.addAnimation(animcolor, forKey: "fillColor")

}


var fillColor = UIColor()
let clipPath = UIBezierPath()
let shapeLayer = CAShapeLayer()

override func drawRect(rect: CGRect) {

        clipPath.moveToPoint(CGPointMake(self.bounds.minX + 7.65, self.bounds.minY - 0.25))
        clipPath.addCurveToPoint(CGPointMake(self.bounds.minX + 7.65, self.bounds.minY + 36.1), controlPoint1: CGPointMake(self.bounds.minX - 2.38, self.bounds.minY + 9.79), controlPoint2: CGPointMake(self.bounds.minX - 2.38, self.bounds.minY + 26.06))
        clipPath.addCurveToPoint(CGPointMake(self.bounds.minX + 43.99, self.bounds.minY + 36.1), controlPoint1: CGPointMake(self.bounds.minX + 17.69, self.bounds.minY + 46.13), controlPoint2: CGPointMake(self.bounds.minX + 33.96, self.bounds.minY + 46.13))
        clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 43.99, self.bounds.minY + 36.1))
        clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 44.01, self.bounds.minY + 0.19))
        clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 7.58, self.bounds.minY + 0.19))
        clipPath.usesEvenOddFillRule = true
        fillColor = userColor
    }

clipPath.addClip()
fillColor.setFill()
clipPath.fill()
shapeLayer.path = clipPath.CGPath
self.layer.mask = shapeLayer
Hamish
  • 78,605
  • 19
  • 187
  • 280
Max Phillips
  • 6,991
  • 9
  • 44
  • 71

2 Answers2

3

Just like changing the background color of a UIView is what I'm looking for.

But why can't you do exactly that? You're already masking the view to your path – so animating your background color would achieve the exact effect you want. Just get rid of the Core Graphics path filling – and also remove the drawRect override, as you can't use drawRect with an animation of the background.

If you really need to use drawRect and animate the color, you could consider splitting up your UIView into two UIViews - one to draw the background layer (which you could animate), and one to draw the custom drawing. Alternatively, you could setup a CADisplayLink to re-draw the view every frame with an intermediate background color – but that's not amazing in terms of performance.

As I say below, you can't animate the fill color of a UIBezierPath directly. If you only want to change (instead of animate) the fill color with your existing code, then you'd just want to call setNeedsDisplay with a different userColor.

If you're asking this because you're using two different paths for the masking and filling, see my below (overcomplicated in hindsight) answer.


The problem is the fill color of a UIBezierPath isn't animatable. But the fill color on a CAShapeLayer is.

Therefore you want to use another CAShapeLayer for the filling instead. I suggest you create a fillLayer and a maskLayer property in order to make their functions clear. Now because you're using a 100% layered approach, you can move your code out of the drawRect, as you're no longer doing any Core Graphics drawing.

Also, it's worth noting that you need to use a CGColor for the animation to and from values.

Something like this should do the trick:

var fillColor = UIColor.greenColor()
let maskLayer = CAShapeLayer()
let fillLayer = CAShapeLayer()

func changeBackgroundColor() {

    let animcolor = CABasicAnimation(keyPath: "fillColor")
    animcolor.fromValue = UIColor.greenColor().CGColor
    animcolor.toValue = UIColor.orangeColor().CGColor
    animcolor.duration = 1.0;
    animcolor.repeatCount = 0;
    animcolor.autoreverses = true
    fillLayer.addAnimation(animcolor, forKey: "fillColor")

}

// re-adjust the clipping path when the view bounds changes
override func layoutSubviews() {

    let clipPath = UIBezierPath()
    clipPath.moveToPoint(CGPointMake(self.bounds.minX + 7.65, self.bounds.minY - 0.25))
    clipPath.addCurveToPoint(CGPointMake(self.bounds.minX + 7.65, self.bounds.minY + 36.1), controlPoint1: CGPointMake(self.bounds.minX - 2.38, self.bounds.minY + 9.79), controlPoint2: CGPointMake(self.bounds.minX - 2.38, self.bounds.minY + 26.06))
    clipPath.addCurveToPoint(CGPointMake(self.bounds.minX + 43.99, self.bounds.minY + 36.1), controlPoint1: CGPointMake(self.bounds.minX + 17.69, self.bounds.minY + 46.13), controlPoint2: CGPointMake(self.bounds.minX + 33.96, self.bounds.minY + 46.13))
    clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 43.99, self.bounds.minY + 36.1))
    clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 44.01, self.bounds.minY + 0.19))
    clipPath.addLineToPoint(CGPointMake(self.bounds.minX + 7.58, self.bounds.minY + 0.19))

    // update layer paths
    fillLayer.path = clipPath.CGPath
    maskLayer.path = clipPath.CGPath

}

override init(frame: CGRect) {
    super.init(frame: frame)

    // configure filling
    fillLayer.fillColor = fillColor.CGColor
    fillLayer.fillRule = kCAFillRuleEvenOdd

    // add fill path to superlayer
    layer.addSublayer(fillLayer)

    // configure masking layer
    maskLayer.fillRule = kCAFillRuleEvenOdd
    layer.mask = maskLayer
}
Community
  • 1
  • 1
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • That is awesome. Thank you for working that into a better implementation. However I can not get the fill color of `fillLayer` to animate, I can't even get it to change. The function is definitely being called, I'm logging it to make sure. This is the kind of thing where I sit down and think I'll crank it out in 30-40 minutes and it ends up taking half the day. – Max Phillips Apr 10 '16 at 22:05
  • @spacemonkey Is the `fillLayer` being displayed on-screen prior to animating? Make sure it's `frame` is non-zero, and it's added to the layer hierarchy. If you're using my above code, you'll need to initialise the `UIView` with the `init(frame:)` initialiser (in reality you'd want to move any size related code into a more suitable place, like `layoutSubviews` for instance). – Hamish Apr 10 '16 at 22:09
  • Oh it's there. Exactly where, and the size, it should be. The fill color is set to what it should be as well, it just won't change to something else. – Max Phillips Apr 10 '16 at 22:10
  • Hmm... weird. The code above is working fine for me in a test project. Did you make sure you added the `.CGColor` to the to and from values of the animation? – Hamish Apr 10 '16 at 22:12
  • I know right, this is crazy. Even commented out the entire class and entered it verbatim. – Max Phillips Apr 10 '16 at 22:18
  • @spacemonkey Dare I suggest cleaning your build folder and relaunching Xcode? – Hamish Apr 10 '16 at 22:19
  • Yeah you totally nailed it. Of course I called it on the wrong instance. Thank you for all of your help! – Max Phillips Apr 10 '16 at 22:45
1

Swift 4 version, you can check code in playground

var fillColor = UIColor.green
let maskLayer = CAShapeLayer()
let fillLayer = CAShapeLayer()

func changeBackgroundColor() {
    
    let animcolor = CABasicAnimation(keyPath: "fillColor")
    animcolor.fromValue = UIColor.green.cgColor
    animcolor.toValue = UIColor.orange.cgColor
    animcolor.duration = 1.0;
    animcolor.repeatCount = 0;
    animcolor.autoreverses = true
    fillLayer.add(animcolor, forKey: "fillColor")
    
}

// re-adjust the clipping path when the view bounds changes

let view = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
view.backgroundColor = UIColor.white

func createPaths() {
    let clipPath = UIBezierPath()
    clipPath.move(to: CGPoint(x: view.bounds.minX + 7.65, y: view.bounds.minY - 0.25))
    clipPath.addCurve(to: CGPoint(x: view.bounds.minX + 7.65, y: view.bounds.minY + 36.1),
                      controlPoint1: CGPoint(x: view.bounds.minX - 2.38, y: view.bounds.minY + 9.79),
                      controlPoint2: CGPoint(x: view.bounds.minX - 2.38, y: view.bounds.minY + 26.06))
    clipPath.addCurve(to: CGPoint(x: view.bounds.minX + 43.99, y: view.bounds.minY + 36.1),
                      controlPoint1: CGPoint(x: view.bounds.minX + 17.69, y: view.bounds.minY + 46.13),
                      controlPoint2: CGPoint(x: view.bounds.minX + 33.96, y: view.bounds.minY + 46.13))
    
    clipPath.addLine(to: CGPoint(x: view.bounds.minX + 43.99, y: view.bounds.minY + 36.1))
    clipPath.addLine(to: CGPoint(x: view.bounds.minX + 44.01, y: view.bounds.minY + 0.19))
    clipPath.addLine(to: CGPoint(x: view.bounds.minX + 7.58, y: view.bounds.minY + 0.19))
    
    // update layer paths
    fillLayer.path = clipPath.cgPath
    maskLayer.path = clipPath.cgPath
    
    fillLayer.fillColor = fillColor.cgColor
    fillLayer.fillRule = .evenOdd
    view.layer.addSublayer(fillLayer)
    maskLayer.fillRule = .evenOdd
    view.layer.mask = maskLayer
}

createPaths()
Bimawa
  • 3,535
  • 2
  • 25
  • 45