ADDC 2017 - John Sundell: Creating great animations on iOS

ADDC 2017 - John Sundell: Creating great animations on iOS

Animations are a huge part of modern application design - and on iOS we have quite a number of tools at our disposal to create great ones to delight & inform our users. Durning this talk, John will go through several animation techniques and talk about how they can be used to create a smooth workflow for both developers & designers - and resulting in a great user experience.

More about the talk, authors & slides: https://addconf.com/talks/7/
Read about the conference: https://addconf.com

Transcript

  1. ⚒ Tools " Techniques # Teamwork

  2. ♻ Tweaks & iterations

  3. % Smooth, flexible workflows

  4. % Smooth, flexible workflows

  5. ⚒ Tools " Techniques

  6. & Scene-based % View-based ' Frame-based

  7. None
  8. None
  9. None
  10. SpriteKit Apple’s framework for 2D game development

  11. ' SKScene

  12. ( SKSpriteNode ' SKScene

  13. None
  14. None
  15. None
  16. None
  17. Hierarchy matching the Sketch file )

  18. Hierarchy matching the Sketch file ) * SKTextureAtlas + .spriteatlas

  19. None
  20. , SKAction

  21. Jump let jumpAction = SKAction.sequence([ .moveBy(x: 0, y: 3, duration:

    0.1), .moveBy(x: 0, y: -3, duration: 0.1) ])
  22. Jump let jumpAction = SKAction.sequence([ .moveBy(x: 0, y: 3, duration:

    0.1), .moveBy(x: 0, y: -3, duration: 0.1) ]) let carAction = SKAction.sequence([ jumpAction .wait(forDuration: 2) ])
  23. Jump let jumpAction = SKAction.sequence([ .moveBy(x: 0, y: 3, duration:

    0.1), .moveBy(x: 0, y: -3, duration: 0.1) ]) let carAction = SKAction.sequence([ jumpAction .wait(forDuration: 2) ]) carNode.run(.repeatForever(carAction))
  24. Jump let jumpAction = SKAction.sequence([ .moveBy(x: 0, y: 3, duration:

    0.1), .moveBy(x: 0, y: -3, duration: 0.1) ]) let wheelAction = SKAction.sequence([ .rotate(byAngle: .pi * 2, duration: 1.5) jumpAction ]) wheelNode.run(.repeatForever(wheelAction)) Rotate & jump let carAction = SKAction.sequence([ jumpAction .wait(forDuration: 2) ]) carNode.run(.repeatForever(carAction))
  25. Jump let jumpAction = SKAction.sequence([ .moveBy(x: 0, y: 3, duration:

    0.1), .moveBy(x: 0, y: -3, duration: 0.1) ]) let wheelAction = SKAction.sequence([ .rotate(byAngle: .pi * 2, duration: 1.5) jumpAction ]) wheelNode.run(.repeatForever(wheelAction)) Rotate & jump let carAction = SKAction.sequence([ jumpAction .wait(forDuration: 2) ]) carNode.run(.repeatForever(carAction))
  26. ) Great for complex stand-alone scenes - Easy to compose

    actions to build animations . Create hierarchies matching the asset structure ♻ Actions can be reused and their parameters can be easily tweaked SpriteKit
  27. Button Label ' % View-based animations

  28. UIView.animate(withDuration: 0.3) { button.frame.size = CGSize(width: 200, height: 200) }

    Button
  29. UIView.animate(withDuration: 0.3) { button.frame.size = CGSize(width: 200, height: 200) }

    Button
  30. button.alpha = 0 UIView.animate(withDuration: 0.3, animations: { button.alpha = 1

    }, completion: { _ in UIView.animate(withDuration: 0.3) { button.frame.size = CGSize(width: 200, height: 200) } })
  31. We can do better! /

  32. Button Button Button

  33. button.animate([ .fadeIn(duration: 0.3), .scale(to: CGSize(width: 200, height: 200), duration: 0.3)

    ]) Button Button Button
  34. struct Animation { var duration: TimeInterval var closure: (UIView) ->

    Void }
  35. struct Animation { var duration: TimeInterval var closure: (UIView) ->

    Void } extension Animation { } static func fadeIn(duration: TimeInterval) -> Animation { return Animation(duration: duration, closure: { $0.alpha = 1 }) } static func scale(to size: CGSize, duration: TimeInterval) -> Animation { return Animation(duration: duration, closure: { $0.frame.size = size }) }
  36. extension UIView { func animate(_ animations: [Animation]) { guard !animations.isEmpty

    else { return } var animations = animations let animation = animations.removeFirst() UIView.animate(withDuration: animation.duration, animations: { animation.closure(self) }, completion: { _ in self.animate(animations) }) } }
  37. extension UIView { func animate(_ animations: [Animation]) { guard !animations.isEmpty

    else { return } var animations = animations let animation = animations.removeFirst() UIView.animate(withDuration: animation.duration, animations: { animation.closure(self) }, completion: { _ in self.animate(animations) }) } } Exit condition
  38. extension UIView { func animate(_ animations: [Animation]) { guard !animations.isEmpty

    else { return } var animations = animations let animation = animations.removeFirst() UIView.animate(withDuration: animation.duration, animations: { animation.closure(self) }, completion: { _ in self.animate(animations) }) } } Exit condition Take out the next animation
  39. extension UIView { func animate(_ animations: [Animation]) { guard !animations.isEmpty

    else { return } var animations = animations let animation = animations.removeFirst() UIView.animate(withDuration: animation.duration, animations: { animation.closure(self) }, completion: { _ in self.animate(animations) }) } } Exit condition Take out the next animation Perform the animation
  40. extension UIView { func animate(_ animations: [Animation]) { guard !animations.isEmpty

    else { return } var animations = animations let animation = animations.removeFirst() UIView.animate(withDuration: animation.duration, animations: { animation.closure(self) }, completion: { _ in self.animate(animations) }) } } Exit condition Take out the next animation Perform the animation Recursively call the method
  41. iHungry 0 1 2 3 4 5 6 7 8

    9 : ; < = > ?
  42. iHungry 0 1 2 3 4 5 6 7 8

    9 : ; < = >
  43. burrito.animate([ .scale(to: burrito.frame.size * 1.5, duration: 0.3), .move(to: basket.center, duration:

    0.3) ]) iHungry 0 1 2 3 4 5 6 7 8 9 : ; < = >
  44. burrito.animate([ .scale(to: burrito.frame.size * 1.5, duration: 0.3), .move(to: basket.center, duration:

    0.3) ]) burrito.animate(inParallel: [ .scale(to: burrito.frame.size * 0.5, duration: 0.3), .fadeOut(duration: 0.3) ]) iHungry 0 1 2 3 4 5 6 7 8 9 : ; < = >
  45. burrito.animate([ .scale(to: burrito.frame.size * 1.5, duration: 0.3), .move(to: basket.center, duration:

    0.3) ]) burrito.animate(inParallel: [ .scale(to: burrito.frame.size * 0.5, duration: 0.3), .fadeOut(duration: 0.3) ]) basket.animate([ .scale(to: basket.frame.size * 1.5, duration: 0.3), .scale(to: basket.frame.size, duration: 0.3) ]) iHungry 0 1 2 3 4 5 6 7 8 9 : ; < = >
  46. burrito.animate([ .scale(to: burrito.frame.size * 1.5, duration: 0.3), .move(to: basket.center, duration:

    0.3) ]) burrito.animate(inParallel: [ .scale(to: burrito.frame.size * 0.5, duration: 0.3), .fadeOut(duration: 0.3) ]) basket.animate([ .scale(to: basket.frame.size * 1.5, duration: 0.3), .scale(to: basket.frame.size, duration: 0.3) ]) badge.animate(.fadeIn(duration: 0.3)) iHungry 0 1 2 3 4 5 6 7 8 9 : ; < = > 1
  47. Demo

  48. ' Frame-based animations @ & A

  49. None
  50. Animation( name: "swordsman-moving-right", frameCount: 8, duration: 1.12 )

  51. Animation( name: "swordsman-moving-right", frameCount: 8, duration: 1.12 )

  52. Animation( name: "swordsman-moving-right", frameCount: 8, duration: 1.12 ) >100

  53. + Graphics swordsman-moving-right-0@2x.png swordsman-moving-right-1@2x.png swordsman-moving-right-2@2x.png swordsman-moving-right-3@2x.png swordsman-moving-right-4@2x.png swordsman-moving-right-5@2x.png swordsman-moving-right-6@2x.png swordsman-moving-right-7@2x.png

  54. + Graphics 0@2x.png 1@2x.png 2@2x.png 3@2x.png 4@2x.png 5@2x.png 6@2x.png 7@2x.png

    + Swordsman + Moving + Right
  55. + Graphics 0@2x.png 1@2x.png 2@2x.png 3@2x.png 4@2x.png 5@2x.png 6@2x.png 7@2x.png

    + Swordsman + Moving + Right Duration.txt B
  56. + Graphics 0@2x.png 1@2x.png 2@2x.png 3@2x.png 4@2x.png 5@2x.png 6@2x.png 7@2x.png

    + Swordsman + Moving + Right Duration.txt B 1.12
  57. Animation( name: "Swordsman/Right/Moving", frameCount: 8, duration: 1.12 )

  58. Animation( name: "Swordsman/Right/Moving", frameCount: 8, duration: 1.12 ) Folder path

  59. Animation( name: "Swordsman/Right/Moving", frameCount: 8, duration: 1.12 ) Folder path

    Number of images
  60. Animation( name: "Swordsman/Right/Moving", frameCount: 8, duration: 1.12 ) Folder path

    Number of images Duration.txt
  61. , Script Script

  62. , Script Script Characters

  63. , Script Script + Folders Characters

  64. , Script Script + Folders Characters ⚡ Generate code

  65. + Swordsman/Moving/Right 0@2x.png 1@2x.png 2@2x.png 3@2x.png 4@2x.png 5@2x.png 6@2x.png 7@2x.png

    Duration.txt B Animation( name: "Swordsman/Right/Moving", frameCount: , duration: 1.12 ) 8
  66. + Swordsman/Moving/Right 0@2x.png 1@2x.png 2@2x.png 3@2x.png 4@2x.png 5@2x.png 6@2x.png 7@2x.png

    Duration.txt B Animation( name: "Swordsman/Right/Moving", frameCount: , duration: 1.12 ) 8@2x.png 9
  67. %

  68. % ⚒ Tools

  69. % D Organize ⚒ Tools

  70. % D Organize B Configs ⚒ Tools

  71. % , Scripts D Organize B Configs ⚒ Tools

  72. % , Scripts D Organize B Configs ⚒ Tools EF

  73. % , Scripts D Organize B Configs ⚒ Tools EF

    G H
  74. % , Scripts D Organize B Configs ⚒ Tools E

    F G H
  75. @johnsundell