Introduction
If you are not familiar with PaintCode – it’s a widely used and very useful little program, that can save you a lot of programming time for simple things like a logo animation. It’s main idea is that you draw a canvas that the app then turns into code.
Here is there website, if you would like to check them out: PaintCode , current price right now is $99, which I think it’s worth it. You could also download a trial version.
There is a lot you can do with PC, you could animate stuff and it’s very simple to implement in iOS development and Swift, and that’s how is done in the companies too, recruiters often hire developers on LinkedIn for big outsourcing project from top tech firms like Google, Amazon, Microsoft and more.
For this little tutorial we’ll go over creating a simple loading animation.
PaintCode stuff
I’ve create a very simple canvas, basically added 4 ovals, stroke only, then used the Oval options on the right to change the start and end of each one, so they are only a quarter of the circle ( 90 degrees ), unchecked ‘closed’ , so it’s just the outside wall. Then I added some shadow and a grayish oval in the middle with low opacity, only fill and no stroke.
You can create the same, similar or something totally different.
The trick to animate it now is in the variables in the bottom left. For the purpose of this tutorial I want each of the circles to turn around, essentially 360 degrees. So we create 4 variables on the left , of type Expression.
Since I want to move all of them at the same time I’ll create another variable, called Master, which is just going to be a Fraction, or with other words – between 0.0 and 1.0 . Then we tie all the angles with the master ( make sure you offset each colored circle by the additional 90 degrees ):
Now if you play around with the Master slider you should see all the circles animate accordingly:
You can directly the code from within PaintCode for this one, but usually you would just go to File > Export… then do StyleKit:
Xcode stuff
Now create a new XCode project > Single Page App:
Drag the CircleLoad.swift file into your project. Here is mine:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
// // CircleLoad.swift // CircleLoad // // Created by Razvigor Andreev on 2/25/16. // Copyright (c) 2016 helpMeCodeSwift. All rights reserved. // // Generated by PaintCode (www.paintcodeapp.com) // import UIKit public class CircleLoad : NSObject { //// Drawing Methods public class func drawCanvas1(master master: CGFloat = 0.538) { //// General Declarations let context = UIGraphicsGetCurrentContext() //// Color Declarations let color = UIColor(red: 0.091, green: 0.699, blue: 0.292, alpha: 1.000) let color2 = UIColor(red: 0.000, green: 0.000, blue: 0.000, alpha: 1.000) let color3 = UIColor(red: 0.078, green: 0.420, blue: 1.000, alpha: 1.000) let color4 = UIColor(red: 0.977, green: 0.652, blue: 0.000, alpha: 1.000) let color6 = UIColor(red: 0.654, green: 0.700, blue: 0.789, alpha: 0.219) //// Shadow Declarations let shadow = NSShadow() shadow.shadowColor = UIColor.blackColor().colorWithAlphaComponent(0.86) shadow.shadowOffset = CGSizeMake(1.1, 1.1) shadow.shadowBlurRadius = 5 //// Variable Declarations let rotationOrange: CGFloat = master * 360 - 90 let rotationBlue: CGFloat = master * 360 let rotationBlack: CGFloat = master * 360 - 180 let rotationGreen: CGFloat = master * 360 - 270 //// Group //// Group 2 CGContextSaveGState(context) CGContextSetShadowWithColor(context, shadow.shadowOffset, shadow.shadowBlurRadius, (shadow.shadowColor as! UIColor).CGColor) CGContextBeginTransparencyLayer(context, nil) //// Oval 4 Drawing CGContextSaveGState(context) CGContextTranslateCTM(context, 61.7, 63.3) CGContextRotateCTM(context, -rotationOrange * CGFloat(M_PI) / 180) CGContextScaleCTM(context, 0.9, 0.9) let oval4Rect = CGRectMake(-63, -63, 126, 126) let oval4Path = UIBezierPath() oval4Path.addArcWithCenter(CGPointMake(oval4Rect.midX, oval4Rect.midY), radius: oval4Rect.width / 2, startAngle: -90 * CGFloat(M_PI)/180, endAngle: 0 * CGFloat(M_PI)/180, clockwise: true) color4.setStroke() oval4Path.lineWidth = 2.5 oval4Path.stroke() CGContextRestoreGState(context) //// Oval 2 Drawing CGContextSaveGState(context) CGContextTranslateCTM(context, 61.7, 63.3) CGContextRotateCTM(context, -rotationBlack * CGFloat(M_PI) / 180) CGContextScaleCTM(context, 0.9, 0.9) let oval2Rect = CGRectMake(-63, -63, 126, 126) let oval2Path = UIBezierPath() oval2Path.addArcWithCenter(CGPointMake(oval2Rect.midX, oval2Rect.midY), radius: oval2Rect.width / 2, startAngle: -90 * CGFloat(M_PI)/180, endAngle: 0 * CGFloat(M_PI)/180, clockwise: true) color2.setStroke() oval2Path.lineWidth = 2.5 oval2Path.stroke() CGContextRestoreGState(context) //// Oval 3 Drawing CGContextSaveGState(context) CGContextTranslateCTM(context, 61.7, 63.3) CGContextRotateCTM(context, -rotationGreen * CGFloat(M_PI) / 180) CGContextScaleCTM(context, 0.9, 0.9) let oval3Rect = CGRectMake(-63, -63, 126, 126) let oval3Path = UIBezierPath() oval3Path.addArcWithCenter(CGPointMake(oval3Rect.midX, oval3Rect.midY), radius: oval3Rect.width / 2, startAngle: -90 * CGFloat(M_PI)/180, endAngle: 0 * CGFloat(M_PI)/180, clockwise: true) color.setStroke() oval3Path.lineWidth = 2.5 oval3Path.stroke() CGContextRestoreGState(context) //// Oval Drawing CGContextSaveGState(context) CGContextTranslateCTM(context, 61.7, 63.3) CGContextRotateCTM(context, -rotationBlue * CGFloat(M_PI) / 180) CGContextScaleCTM(context, 0.9, 0.9) let ovalRect = CGRectMake(-63, -63, 126, 126) let ovalPath = UIBezierPath() ovalPath.addArcWithCenter(CGPointMake(ovalRect.midX, ovalRect.midY), radius: ovalRect.width / 2, startAngle: -90 * CGFloat(M_PI)/180, endAngle: 0 * CGFloat(M_PI)/180, clockwise: true) color3.setStroke() ovalPath.lineWidth = 2.5 ovalPath.stroke() CGContextRestoreGState(context) //// Oval 5 Drawing CGContextSaveGState(context) CGContextTranslateCTM(context, 61.7, 63.3) CGContextScaleCTM(context, 0.85, 0.85) let oval5Path = UIBezierPath(ovalInRect: CGRectMake(-63, -63, 126, 126)) color6.setFill() oval5Path.fill() CGContextRestoreGState(context) CGContextEndTransparencyLayer(context) CGContextRestoreGState(context) } } |
Feel free to use this one or the one you create. As you can see the export file is a subclass of NSObject and it’s using CoreGraphics to draw everything.
We need to create a custom UIView, that can use this NSObject class as a drawing method.
So, create a new file, CocoaTouchClass, subclass of UIView ( I named mine CircleView ).
Here is the code for this – it’s very simple:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// // CircleView.swift // LoadingLikeABoss // // Created by Razvigor Andreev on 2/22/16. // Copyright © 2016 Razvigor Andreev. All rights reserved. // import UIKit @IBDesignable public class CircleView: UIView { var masterSlider: CGFloat = 0 override public func awakeFromNib() { super.awakeFromNib() } required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } override init(frame: CGRect) { super.init(frame: frame) } override public func drawRect(rect: CGRect) { CircleLoad.drawCanvas1(master: masterSlider) } } |
What we are doing is overriding the default drawRect method of UIView with the newly create drawing method from PaintCode. However I added a variable masterSlider, because we’ll need a way to control it from outside.
Now add a new UIView to your UIViewController and two UIButtons. Make the view a subclass of our
new ‘CircleView’ class:
Also drag IBOutlet for the CircleView and an IBAction for each of the buttons.
1 2 3 4 5 6 7 8 |
@IBAction func startTapped(sender: AnyObject) { } @IBAction func stopTapped(sender: AnyObject) { } @IBOutlet weak var circleLoad: CircleView! |
Here would be a good time to talk about how we are going to animate this.
The idea is that we can call ‘circleLoad.masterSlider = 0.5’ and this should animate our new CircleView, however we need to tell the View that it needs to redraw. How you do that is by calling: ‘circleLoad.setNeedsDisplay()’
So in theory you should be able to do a ‘for loop’ or ‘while loop’, something like:
1 2 3 4 5 |
var i : CGFloat = 0 for i = 0.0; i <= 1.0; i += 0.01 { circleLoad.masterSlider = i circleLoad.setNeedsDisplay() } |
However, if you try something like this, you will see that it will not work.
The masterSlider will be changed every time, but the setNeedsDisplay() will be called only one time. This is how this was designed by Apple and there are different workarounds, but here is how we are going to do it – using NSTimer. Let’s add a few variables in your UIViewController:
1 2 3 4 |
var timer: NSTimer! var timerOn: Bool = false var current: CGFloat = 0 var limit: CGFloat = 1 |
- ‘timer‘ – will be our NSTimer, responsible for updating the View
- ‘timerOn‘ – will be used to check if the timer is running, as we don’t want to start it twice
- ‘current‘ – will be a local variable to save the masterSlider current value
- ‘limit‘ – will be used in case you want a limit for the masterSlider
Let’s create our animation function:
1 2 3 4 5 6 7 |
func startAnimation() { circleLoad.masterSlider = current circleLoad.setNeedsDisplay() current += 0.05 } |
Pretty straight forward – set the view’s masterSlider to whatever the current offset is, redraw and increase the current by the step that I chose of 0.05 ( the smaller the step, the smoother the animation )
Now let’s create a new function to fire off the NSTimer:
1 2 3 4 5 6 |
func startTimer() { if !timerOn { timerOn = true timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("startAnimation"), userInfo: nil, repeats: true) } else { print("Animation Already running") } } |
First we check if our Timer is already running, if not – then start it and repeat it every 0.1s ( feel free to change the repeat interval ), if yes – just print some system error or anything else that you would like.
One thing to make sure is that in the Selector(“FUNCTION NAME”) , the FUNCTION NAME is exactly the name of your startAnimation function, were you to name it something different.
Now let’s finish by creating a function to stop the timer:
1 2 3 4 5 6 7 8 9 |
func stopTimer() { timer.invalidate() timerOn = false current = 0 circleLoad.masterSlider = 1 circleLoad.setNeedsDisplay() } |
Finally call the correct functions inside each button action:
1 2 3 4 5 6 |
@IBAction func startTapped(sender: AnyObject) { startTimer() } @IBAction func stopTapped(sender: AnyObject) { stopTimer() } |
Run your project and you should have a nice loading animation like this:
There is a lot more you can do with PaintCode, but this should get you started 🙂
If you have any questions, please post them below.
You can find the full project here at my GitHub repo: LoadingLikeABoss
Cheers!
Couldn’t you have grouped all of the colors and just rotated that group? Seems a little overkill to have a master slider to rotate all four layers.
Of course it is 🙂 However if you do it this way with layers it gives you more options to create effects later if you wanted. For example you could make each 1/4 circle different lengths or travel at different speeds and overlap each other. It’s just an example 😉 If you wanted to just create this specific loading icon you could actually just draw the circle and rotate it with an action – much easier 😉