iOS UIView Animation

来源:互联网 发布:微信公众号 知乎 编辑:程序博客网 时间:2024/05/10 13:54

ANIMATIONS IN SWIFT (PART II)


Thursday August 7, 2014

In Prototyping iOS Animations in Swift I introduced the a range of block-based functions that UIKit provides to create tweened animations and how a simple animation can be programmatically altered with some random variation to create more complex scenes.

This alone can create an interesting range of animations, but is still only the tip of the iceberg of what Apple provides as tools for creating animations.

This tutorial looks at some more animation functions that require a little bit more setting up to create, but once mastered opens up an even larger number of possibilities.

Container view transitions

If you want to perform an animated transition between two views, Apple provides a handful of default animations that are easy to create with just a small bit of code.

The trick to using these methods is that the transition needs to be performed in a parent container, which is typically just an invisible UIView that’s the size of the largest object you’re transitioning with. So performing this animation requires a little bit of setting up.

For example, to animate a transition between two colored UIViews we’ll use a third UIView as the container for the animation.

Lets set up all three views in the viewDidLoad() function…

let container = UIView()let redSquare = UIView()let blueSquare = UIView()override func viewDidLoad() {    super.viewDidLoad()        // set container frame and add to the screen    self.container.frame = CGRect(x: 60, y: 60, width: 200, height: 200)    self.view.addSubview(container)        // set red square frame up    // we want the blue square to have the same position as redSquare     // so lets just reuse blueSquare.frame    self.redSquare.frame = CGRect(x: 0, y: 0, width: 200, height: 200)    self.blueSquare.frame = redSquare.frame        // set background colors    self.redSquare.backgroundColor = UIColor.redColor()    self.blueSquare.backgroundColor = UIColor.blueColor()        // for now just add the redSquare    // we'll add blueSquare as part of the transition animation     self.container.addSubview(self.redSquare)   }

Running the app now all you should see is a redSquare:

Now lets add an @IBAction from a button in the storyboard to trigger the actual animation and use the functionUIView.transitionWithView which takes these options:

  • view: the view that the transition is animated within
  • duration: the number of seconds the transition will take
  • options: options for the transition (e.g. the animation style to use)
  • animations: a block defining changes to make as part of the transition; and
  • completion: another block this one defining code to run when the animation has completed.

The animation block should typically include the removal of one view, and the addition of the view (to the container) that is replacing it.

@IBAction func animateButtonTapped(sender: AnyObject) {  // create a 'tuple' (a pair or more of objects assigned to a single variable)let views = (frontView: self.redSquare, backView: self.blueSquare)// set a transition stylelet transitionOptions = UIViewAnimationOptions.TransitionCurlUpUIView.transitionWithView(self.container, duration: 1.0, options: transitionOptions, animations: {    // remove the front object...    views.frontView.removeFromSuperview()       // ... and add the other object    self.container.addSubview(views.backView)   }, completion: { finished in    // any code entered here will be applied    // .once the animation has completed})}

The transition from red square to blue square works as expected, but after that we’re stuck with the blue square.

This is because our @IBAction function is set up with the expectation that the redSquare is the visible square, and this is only true the first time we tap the animate button.

To fix this we’ll need to add some conditional logic that checks to see which square is currently visible to figure out if we should the transitioning from red-to-blue, or blue-to-red.

There are lots of ways this could be done, but lets use a feature of Swift called a ‘tuple’1.

// create a 'tuple' (a pair or more of objects assigned to a single variable)var views : (frontView: UIView, backView: UIView)// if redSquare has a superView (e.g it's in the container)// set redSquare as front, and blueSquare as back// otherwise flip the orderif(self.redSquare.superview){    views = (frontView: self.redSquare, backView: self.blueSquare)}else {    views = (frontView: self.blueSquare, backView: self.redSquare)}

Now we can alternate between the red and blue views!

Switching from one view to another is such a common task that so long as we don’t have anything else we also want to animate with the transition, Apple provides a slightly easier function that does the removeFromSuperview() and addSubView() for you automatically.

Here’s our simplified @IBAction function using this simplified transition function:

@IBAction func animateButtonTapped(sender: AnyObject) {        // create a 'tuple' (a pair or more of objects assigned to a single variable)    var views : (frontView: UIView, backView: UIView)    if(self.redSquare.superview){        views = (frontView: self.redSquare, backView: self.blueSquare)    }    else {        views = (frontView: self.blueSquare, backView: self.redSquare)    }        // set a transition style    let transitionOptions = UIViewAnimationOptions.TransitionCurlUp    // with no animation block, and a completion block set to 'nil' this makes a single line of code      UIView.transitionFromView(views.frontView, toView: views.backView, duration: 1.0, options: transitionOptions, completion: nil)    }

Now let’s try out some of the different default transition options that are available:

let transitionOptions = UIViewAnimationOptions.TransitionCurlDown

let transitionOptions = UIViewAnimationOptions.TransitionFlipFromLeft

Keyframe block animations

Another new addition in iOS 7, instead of creating an animation by interpolating between a start value to an end value, this method allows us to define an by as many sub-parts as we want.

For one example of why you’d want to do this, consider how you would rotate an image a full 360 degrees.

Using our familiar basic animation functions you could attempt to animate the transform property like so:

// create and add blue-fish.png image to screenlet fish = UIImageView()fish.image = UIImage(named: "blue-fish.png")fish.frame = CGRect(x: 50, y: 50, width: 50, height: 50)self.view.addSubview(fish)// angles in iOS are measured as radians PI is 180 degrees so PI × 2 is 360 degreeslet fullRotation = CGFloat(M_PI * 2)UIView.animateWithDuration(1.0, animations: {    // animating `transform` allows us to change 2D geometry of the object     // like `scale`, `rotation` or `translate`    self.fish.transform = CGAffineTransformMakeRotation(fullRotation)})

But iOS can’t interpolate between the start and end values because they are equivalent!

To get around this, we’ll use animateKeyframesWithDuration to define the rotation in smaller parts, which iOS won’t get confused with, and then combine them all into a single animation.

To do the full rotation, lets break the animation into three parts, with each part rotating one third2 of the way around:

let duration = 2.0let delay = 0.0let options = UIViewKeyframeAnimationOptions.CalculationModeLinearUIView.animateKeyframesWithDuration(duration, delay: delay, options: options, animations: {    // each keyframe needs to be added here    // within each keyframe the relativeStartTime and relativeDuration need to be values between 0.0 and 1.0        UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1/3, animations: {        // start at 0.00s (5s × 0)        // duration 1.67s (5s × 1/3)        // end at   1.67s (0.00s + 1.67s)        self.fish.transform = CGAffineTransformMakeRotation(1/3 * fullRotation)    })    UIView.addKeyframeWithRelativeStartTime(1/3, relativeDuration: 1/3, animations: {        self.fish.transform = CGAffineTransformMakeRotation(2/3 * fullRotation)    })    UIView.addKeyframeWithRelativeStartTime(2/3, relativeDuration: 1/3, animations: {        self.fish.transform = CGAffineTransformMakeRotation(3/3 * fullRotation)    })        }, completion: {finished in        // any code entered here will be applied        // once the animation has completed        })}

Now iOS has enough information to create the animation as we expected.

If you’re manually entering values for relativeStartTime &relativeDuration it’s easy to make a mistake, but if all you want to achieve is a smooth transition between each of the keyframes, you can enter CalculationModePaced as an option which ignores any values you’ve entered for relativeStartTime and relativeDurationand automatically figures out correct values for a consistent animation:

let duration = 2.0let delay = 0.0let options = UIViewKeyframeAnimationOptions.CalculationModePacedUIView.animateKeyframesWithDuration(duration, delay: delay, options: options, animations: {        // note that we've set relativeStartTime and relativeDuration to zero.     // Because we're using `CalculationModePaced` these values are ignored     // and iOS figures out values that are needed to create a smooth constant transition    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {        self.fish.transform = CGAffineTransformMakeRotation(1/3 * fullRotation)    })        UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {        self.fish.transform = CGAffineTransformMakeRotation(2/3 * fullRotation)    })        UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {        self.fish.transform = CGAffineTransformMakeRotation(3/3 * fullRotation)    })    }, completion: nil)

Moving an object along a bezier curve

A really fun animation to create is move the position of an object along a curve.

Our basic animation techniques make it easy to animate an object moving from point A to B, but to have an object move along the multiple points of a curve A,B,C,D,E we’ll need to use a keyframe-based animation again.

You could do this manually using the keyframe block function like we did for the rotation animation, but to get a nice smooth animation we’d have to define a lot of keyframes and it would get quickly get very complicated and messy.

Luckily, instead of assigning each keyframe manually, we can give iOS a bezier curve3 and the keyframes needed will be generated automatically.

This requires us to use more powerful animation features of iOS that are slightly more complicated, but not too hard once you get the general approach.

// first set up an object to animate// we'll use a familiar red squarelet square = UIView()square.frame = CGRect(x: 55, y: 300, width: 20, height: 20)square.backgroundColor = UIColor.redColor()// add the square to the screenself.view.addSubview(square)// now create a bezier path that defines our curve// the animation function needs the curve defined as a CGPath// but these are more difficult to work with, so instead// we'll create a UIBezierPath, and then create a // CGPath from the bezier when we need itlet path = UIBezierPath()path.moveToPoint(CGPoint(x: 16,y: 239))path.addCurveToPoint(CGPoint(x: 301, y: 239), controlPoint1: CGPoint(x: 136, y: 373), controlPoint2: CGPoint(x: 178, y: 110))// create a new CAKeyframeAnimation that animates the objects position let anim = CAKeyframeAnimation(keyPath: "position")// set the animations path to our bezier curveanim.path = path.CGPath// set some more parameters for the animation// this rotation mode means that our object will rotate so that it's parallel to whatever point it is currently on the curve anim.rotationMode = kCAAnimationRotateAutoanim.repeatCount = Float.infinityanim.duration = 5.0// we add the animation to the squares 'layer' propertysquare.layer.addAnimation(anim, forKey: "animate position along path")

Now we have a single animation, lets use it multiple times to create a more complex scene.

// loop from 0 to 5for i in 0...5 {        // create a square     let square = UIView()    square.frame = CGRect(x: 55, y: 300, width: 20, height: 20)    square.backgroundColor = UIColor.redColor()    self.view.addSubview(square)        // randomly create a value between 0.0 and 150.0    let randomYOffset = CGFloat( arc4random_uniform(150))        // for every y-value on the bezier curve    // add our random y offset so that each individual animation    // will appear at a different y-position    let path = UIBezierPath()    path.moveToPoint(CGPoint(x: 16,y: 239 + randomYOffset))    path.addCurveToPoint(CGPoint(x: 301, y: 239 + randomYOffset), controlPoint1: CGPoint(x: 136, y: 373 + randomYOffset), controlPoint2: CGPoint(x: 178, y: 110 + randomYOffset))        // create the animation     let anim = CAKeyframeAnimation(keyPath: "position")    anim.path = path.CGPath    anim.rotationMode = kCAAnimationRotateAuto    anim.repeatCount = Float.infinity    anim.duration = 5.0        // add the animation     square.layer.addAnimation(anim, forKey: "animate position along path")}

Now we have multiple squares animating, but they all start at the same time so don’t look so great.

Lets set some more properties to the animation object that adds some randomness to how long the animation takes (so that some of the squares will move faster than others), and at what position of the animation it starts (so that they appear staggered).

// each square will take between 4.0 and 8.0 seconds// to complete one animation loopanim.duration = Double(arc4random_uniform(40)+30) / 10// stagger each animation by a random value// `290` was chosen simply by experimentationanim.timeOffset = Double(arc4random_uniform(290))

Now our squares follow a bezier curve with with much more variation due to the randomness we’ve added to each animation.

From here, it’s not too much of a step to switch the redSquares to images and add a background to create an interesting organic animation like a school of fish or a flock of birds4.

Animating appearance of a bezier curve

Another useful technique to know that also uses an curve it to animate how much of the curve is drawn.

When we animated an object along a curve, the bezier path wasn’t actually shown on the screen, instead it was used as an input to the keyframe animation.

In this example, we’ll actually draw the curve to the screen, but animate how much of the curve is shown from 0 to 100%.

Lets add this code into an @IBAction function that’s triggered when a button is tapped.

// set up some values to use in the curvelet ovalStartAngle = CGFloat(90.01 * M_PI/180)let ovalEndAngle = CGFloat(90 * M_PI/180)let ovalRect = CGRectMake(97.5, 58.5, 125, 125)// create the bezier pathlet ovalPath = UIBezierPath()ovalPath.addArcWithCenter(CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)),    radius: CGRectGetWidth(ovalRect) / 2,    startAngle: ovalStartAngle,    endAngle: ovalEndAngle, clockwise: true)// create an object that represents how the curve // should be presented on the screenlet progressLine = CAShapeLayer()progressLine.path = ovalPath.CGPathprogressLine.strokeColor = UIColor.blueColor().CGColorprogressLine.fillColor = UIColor.clearColor().CGColorprogressLine.lineWidth = 10.0progressLine.lineCap = kCALineCapRound// add the curve to the screenself.view.layer.addSublayer(progressLine)// create a basic animation that animates the value 'strokeEnd'// from 0.0 to 1.0 over 3.0 secondslet animateStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")animateStrokeEnd.duration = 3.0animateStrokeEnd.fromValue = 0.0animateStrokeEnd.toValue = 1.0// add the animationprogressLine.addAnimation(animateStrokeEnd, forKey: "animate stroke end animation")

This animation uses a very simple oval as a curve, but this technique can be applied to any curve at all. I’ve seen examples where someone has converted cursive text to create the illusion of a word being written, or you could combine it with animating an object along the same curve to show the path it’s taken while animating.

System default animation(s)

Another addition with iOS 7 is UIView.performSystemAnimationwhich for now only has UISystemAnimation.Delete as a valid option but I’m hoping that in the future Apple will add more standard system animations that can be easily created using this function.

// create and add blue-fish.png image to screenlet fish = UIImageView()fish.image = UIImage(named: "blue-fish.png")fish.frame = CGRect(x: 50, y: 50, width: 50, height: 50)self.view.addSubview(fish)// create an array of views to animate (in this case just one)let viewsToAnimate = [fish]// perform the system animation// as of iOS 8 UISystemAnimation.Delete is the only valid optionUIView.performSystemAnimation(UISystemAnimation.Delete, onViews: viewsToAnimate, options: nil, animations: {    // any changes defined here will occur    // in parallel with the system animation }, completion: { finished in     // any code entered here will be applied    // once the animation has completed       })

Fin

Thank you Paul Webb for catching my bad Fin/fish joke in my last post5.

These animations start to get a little more complex so you might not succeed the first time around, but if you’re ever stuck, post a link to your code and I’d be happy to try and help!

Read More Swift Animation Posts...Animations 
Part 1Transitions 
Part 1Transitions 
Part 2Transitions 
Part 3

Notes

  1. Tuples are one of the reasons I love another programming language called Haskell. Most programming languages are designed so that a variable contains a single object. Tuples allow us to create an ad-hoc data structure that contains two (or more) objects of our choice. ↩

  2. Splitting the animation into two halves is still potentially ambiguous since the rotation (in degrees) from 0 to 180 could equally be achieved either by moving clockwise or anti clockwise. Itmight work to define the rotation as (0 to 180 then 180 to 360) but since 0 and 360 are equivalent you might end up with the rotation backtracking on itself. ↩

  3. Making bezier curves from control points isn’t easy to do if you’ve never worked with them outside of vector software before :(

    If you want to do it manually, the best place to start would be David Rönnqvist’s post: Thinking like a bezier path.

    Or, if you’re like me and that’s just too much, download PaintCodewhich (among other useful things) will automatically create Swift code for a bezier curve you’ve drawn on screen. ↩

  4. For this animation I also added a random size for each fish, but instead of randomly adding variation to the speed of each fish I calculated the duration of each animation based off the size so that smaller fish take longer to complete one loop than the larger fish. This helps create an illusion of depth and perspective since closer objects should appear to move faster. ↩

  5. For the record, I’m not a French New Wave fan: sadly it was a reference to Sharknado. ↩

MORE POSTS


THINKING ABOUT BEACONS


PROTOTYPING iOS ANIMATIONS IN SWIFT


ARCHIVE

MATHEW SANDERS IS APRODUCT DESIGNERIN NEW YORK


Currently, I’m a product designer at .

I’m passionate about helping teams find ways to work smarter, and explore ideas faster.

Born & raised in New Zealand, I’m two years new to the wonders of Brooklyn.

My vices are coffee, cats, and science fiction.

If you'd like to know more, please reach out!

  • @permakittens
  • mat@ustwo.com
  • +Mathew Sanders
  • @mathewsanders
  • mathew.tumblr.com
  • keybase.io/permakittens
  • linked.in/mathew.sanders
0 0
原创粉丝点击