2

I just saw this image and it's interesting to me, how to create such type of animation in Swift:

enter image description here

So, I have many gray teeth in circle and when I set the angle, for example 45degree it will fill these gray teeth into blue within 0..45 degree.

You can just explain me the right way of doing it or you can show different snippets(it would be great). And later I will search or read about it.

Thanks in advance!

John Doe
  • 511
  • 1
  • 6
  • 13

2 Answers2

6

If you only need the individual 'teeth' to change color, instead of using the teeth as masks for a solid fill, you can use Core Graphics instead of Core Animation (although Core Animation is generally preferred). So in order to do this, we should be doing the following:

  1. Subclass UIView to insert our drawing code
  2. Create an array of path objects, wrapped in UIBezierPath
  3. Setup a timer to update a progress value and setNeedsDisplay
  4. In drawRect:, draw the paths and assign a fill to each depending on the progress

First of all, lets define the variables we're going to be working with in this UIView subclass.

class TeethLoaderView : UIView {

    let numberOfTeeth = UInt(60) // Number of teeth to render
    let teethSize = CGSize(width:8, height:45) // The size of each individual tooth
    let animationDuration = NSTimeInterval(5.0) // The duration of the animation

    let highlightColor = UIColor(red: 29.0/255.0, green: 175.0/255.0, blue: 255.0/255.0, alpha: 1) // The color of a tooth when it's 'highlighted'
    let inactiveColor = UIColor(red: 233.0/255.0, green: 235.0/255.0, blue: 236.0/255.0, alpha: 1) // The color of a tooth when it isn't 'hightlighted'

    var progress = NSTimeInterval(0.0) // The progress of the loader
    var paths = [UIBezierPath]() // The array containing the UIBezier paths
    var displayLink = CADisplayLink() // The display link to update the progress
    var teethHighlighted = UInt(0) // Number of teeth highlighted

    ...

Now let's add a function to create our paths.

func getPaths(size:CGSize, teethCount:UInt, teethSize:CGSize, radius:CGFloat) -> [UIBezierPath] {

    let halfHeight = size.height*0.5;
    let halfWidth = size.width*0.5;
    let deltaAngle = CGFloat(2*M_PI)/CGFloat(teethCount); // The change in angle between paths

    // Create the template path of a single shape.
    let p = CGPathCreateWithRect(CGRectMake(-teethSize.width*0.5, radius, teethSize.width, teethSize.height), nil);

    var pathArray = [UIBezierPath]()
    for i in 0..<teethCount { // Copy, translate and rotate shapes around

        let translate = CGAffineTransformMakeTranslation(halfWidth, halfHeight);
        var rotate = CGAffineTransformRotate(translate, deltaAngle*CGFloat(i))
        let pathCopy = CGPathCreateCopyByTransformingPath(p, &rotate)!

        pathArray.append(UIBezierPath(CGPath: pathCopy)) // Populate the array
    }

    return pathArray
}

This is fairly simple. We just create a path for a single 'tooth' and then copy this path for how many teeth we need, translating and rotating the path for each one.

Next we want to setup our view. I'm going to a CADisplayLink for the timer so that the animation performs at the same speed on all devices.

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

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    commonSetup()
}

private func commonSetup() {
    self.backgroundColor = UIColor.whiteColor()
    paths = getPaths(frame.size, teethCount: numberOfTeeth, teethSize: teethSize, radius: ((frame.width*0.5)-teethSize.height))

    displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire));
    displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}

Here we just set the background color, as well as setup our timer and initialise the paths we're going to be using. Next we want to setup a function to change the progress of the view when the CADisplayLink fires.

func displayLinkDidFire() {

    progress += displayLink.duration/animationDuration

    if (progress > 1) {
        progress -= 1
    }

    let t = teethHighlighted

    teethHighlighted = UInt(round(progress*NSTimeInterval(numberOfTeeth))) // Calculate the number of teeth to highlight

    if (t != teethHighlighted) { // Only call setNeedsDisplay if the teethHighlighted changed
        setNeedsDisplay()
    }
}

Nothing complicated here, we just update the progress and teethHighlighted and call setNeedsDisplay() to redraw the view, if teethHighlighted changed.

Finally, we want to draw the view.

override func drawRect(rect: CGRect) {

    let ctx = UIGraphicsGetCurrentContext()

    CGContextScaleCTM(ctx, -1, -1) // Flip the context to the correct orientation
    CGContextTranslateCTM(ctx, -rect.size.width, -rect.size.height)

    for (index, path) in paths.enumerate() { // Draw each 'tooth'

        CGContextAddPath(ctx, path.CGPath);

        let fillColor = (UInt(index) <= teethHighlighted) ? highlightColor:inactiveColor;

        CGContextSetFillColorWithColor(ctx, fillColor.CGColor)
        CGContextFillPath(ctx)
    }
}

If you wanted to go down the Core Animation path, I adapted this code into a Core Animation layer


Final Result

enter image description here


Full project: https://github.com/hamishknight/Circle-Loader

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • please, can you say me from where can I learn more about the CoreAnimations? I want to learn it and to know perfect. How did you learn it and from where? – John Doe Jan 23 '16 at 14:12
  • I'm not usually a massive fan of tutorials, I like to go though and pick stuff up myself, using mainly the documentation, but I know there's a good tutorial here: https://www.objc.io/issues/12-animations/animations-explained/ and also here: http://www.raywenderlich.com/113686/ios-animation-tutorial-introduction-easy-animation – Hamish Jan 23 '16 at 14:26
  • sorry, but how can I show animation until some value? I've changed the value of `anim.toValue = 0.6`, but it does not what I want. For example, fill just 180 degree or 50% of whole circle – John Doe Jan 23 '16 at 16:05
  • this answer or the Core Animation answer? For the Core Animation one, setting `anim.toValue = 0.6` should do exactly what you want... what does it do for you? Bear in mind that you'll probably want to delete the `anim.repeatCount = Float.infinity` to stop it from looping. – Hamish Jan 23 '16 at 16:16
  • http://imgur.com/El4Oc3H // look what happens =/ I removed `repeatCount` P.S CoreAnimation – John Doe Jan 23 '16 at 16:47
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/101474/discussion-between-originaluser2-and-john-doe). – Hamish Jan 23 '16 at 16:49
4

Well, in the spirit of "go big or go home" (and because I'm actually having some fun doing this), I created a Core Animation version of my Core Graphics answer. It's quite a bit less code and animates smoother, so I'd actually prefer to use this.

First off, let's subclass a UIView again (this isn't strictly necessary, but it's nice to contain everything in a single view) and define our variables:

class TeethLoaderViewCA : UIView {

    let numberOfTeeth = UInt(60) // Number of teetch to render
    let teethSize = CGSize(width:8, height:45) // The size of each individual tooth
    let animationDuration = NSTimeInterval(5.0) // The duration of the animation

    let highlightColor = UIColor(red: 29.0/255.0, green: 175.0/255.0, blue: 255.0/255.0, alpha: 1) // The color of a tooth when it's 'highlighted'
    let inactiveColor = UIColor(red: 233.0/255.0, green: 235.0/255.0, blue: 236.0/255.0, alpha: 1) // The color of a tooth when it isn't 'hightlighted'

    let shapeLayer = CAShapeLayer() // The teeth shape layer
    let drawLayer = CAShapeLayer() // The arc fill layer

    let anim = CABasicAnimation(keyPath: "strokeEnd") // The stroke animation

    ...

This is mostly the same as the Core Graphics version, but with a couple of Core Animation objects and without the timing logic. Next, we can pretty much copy the getPaths function we created in the other version, except with a few tweaks.

func getPathMask(size:CGSize, teethCount:UInt, teethSize:CGSize, radius:CGFloat) -> CGPathRef? {

    let halfHeight = size.height*0.5
    let halfWidth = size.width*0.5
    let deltaAngle = CGFloat(2*M_PI)/CGFloat(teethCount); // The change in angle between paths

    // Create the template path of a single shape.
    let p = CGPathCreateWithRect(CGRectMake(-teethSize.width*0.5, radius, teethSize.width, teethSize.height), nil)

    let returnPath = CGPathCreateMutable()

    for i in 0..<teethCount { // Copy, translate and rotate shapes around
        let translate = CGAffineTransformMakeTranslation(halfWidth, halfHeight)
        var rotate = CGAffineTransformRotate(translate, deltaAngle*CGFloat(i))
        CGPathAddPath(returnPath, &rotate, p)
    }

    return CGPathCreateCopy(returnPath)
}

This time, all the paths are grouped into one big path and the function returns that path.

Finally, we just have to create our layer objects & setup the animation.

private func commonSetup() {

    // set your background color
    self.backgroundColor = UIColor.whiteColor()

    // Get the group of paths we created.
    shapeLayer.path = getPathMask(frame.size, teethCount: numberOfTeeth, teethSize: teethSize, radius: ((frame.width*0.5)-teethSize.height))

    let halfWidth = frame.size.width*0.5
    let halfHeight = frame.size.height*0.5
    let halfDeltaAngle = CGFloat(M_PI/Double(numberOfTeeth))

    // Creates an arc path, with a given offset to allow it to be presented nicely
    drawLayer.path = UIBezierPath(arcCenter: CGPointMake(halfWidth, halfHeight), radius: halfWidth, startAngle: CGFloat(-M_PI_2)-halfDeltaAngle, endAngle: CGFloat(M_PI*1.5)+halfDeltaAngle, clockwise: true).CGPath
    drawLayer.frame = frame
    drawLayer.fillColor = inactiveColor.CGColor
    drawLayer.strokeColor = highlightColor.CGColor
    drawLayer.strokeEnd = 0
    drawLayer.lineWidth = halfWidth
    drawLayer.mask = shapeLayer
    layer.addSublayer(drawLayer)

    // Optional, but looks nice
    anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
}

All we're doing here is assigning the group of paths to a CAShapeLayer, which we will use as a mask over the drawLayer, which we will be animating around the view (using a stroke on an arch path).


Final Result

enter image description here


Full project: https://github.com/hamishknight/Circle-Loader

Hamish
  • 78,605
  • 19
  • 187
  • 280