├── .gitignore
├── LICENSE
├── README.md
├── assets
├── playground-simple.gif
├── scrubber-chained-1.gif
├── scrubber-simple-1.gif
└── scrubber-simple-2.gif
└── source
└── Animation.playground
├── Pages
├── Keyframe example.xcplaygroundpage
│ └── Contents.swift
├── Simple example.xcplaygroundpage
│ └── Contents.swift
└── Template.xcplaygroundpage
│ └── Contents.swift
├── Sources
└── ScrubContainerView.swift
└── contents.xcplayground
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xcuserstate
23 |
24 | ## Obj-C/Swift specific
25 | *.hmap
26 | *.ipa
27 | *.dSYM.zip
28 | *.dSYM
29 |
30 | ## Playgrounds
31 | timeline.xctimeline
32 | playground.xcworkspace
33 |
34 | # Swift Package Manager
35 | #
36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
37 | # Packages/
38 | .build/
39 |
40 | # CocoaPods
41 | #
42 | # We recommend against adding the Pods directory to your .gitignore. However
43 | # you should judge for yourself, the pros and cons are mentioned at:
44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
45 | #
46 | # Pods/
47 |
48 | # Carthage
49 | #
50 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
51 | # Carthage/Checkouts
52 |
53 | Carthage/Build
54 |
55 | # fastlane
56 | #
57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
58 | # screenshots whenever they are needed.
59 | # For more information about the recommended setup visit:
60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
61 |
62 | fastlane/report.xml
63 | fastlane/Preview.html
64 | fastlane/screenshots
65 | fastlane/test_output
66 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Mathew Sanders
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Playground Animation Scrubber 🎚
2 |
3 | There are a lot of frameworks and tools for making animations and animated
4 | transitions for your iOS projects.
5 |
6 | If you're targeting iOS/tvOS 10.0+, then `UIViewPropertyAnimator` is probably
7 | powerful enough for your needs: It includes the standard easing curves, a nice
8 | implementation of spring physics, an easy way to define your own custom
9 | timing curve, and a scalable API that allows from simple single step
10 | interpolation, to chained, interruptible, and animations that can be modified on
11 | the fly.
12 |
13 | ## ScrubContainerView
14 | This project doesn't modify or build on top of `UIViewPropertyAnimator`, but
15 | instead provides `ScrubContainerView`, a simple UIView subclass, that makes it
16 | easier to create, explore, debug, and refine an animation in a playground.
17 |
18 | 1. Start by making a playground and importing `UIKit` and `PlaygroundSupport`
19 |
20 | ````Swift
21 | import UIKit
22 | import PlaygroundSupport
23 | ````
24 |
25 | 2. Create a new `ScrubContainerView` and make that the playground's `liveView`
26 | (this presents the view in the playground's assistant editor panel.
27 |
28 | ````Swift
29 | let container = ScrubContainerView()
30 | PlaygroundPage.current.liveView = container
31 | ````
32 |
33 | 3. Next, go ahead an set up the objects to be animated, adding them to the
34 | `stage` view of `ScrubContainerView` and setting their initial positions.
35 |
36 | ````Swift
37 | let square = UIView()
38 | container.stage.addSubview(square)
39 | square.center = container.stage.center
40 | square.transform = .identity
41 | square.bounds.size = CGSize(width: 150, height: 50)
42 | square.backgroundColor = .red
43 | ````
44 |
45 | 4. Finally, assign `animator` with a closure that returns a
46 | `UIViewPropertyAnimator`. This defines the animations to perform.
47 |
48 | ````Swift
49 | container.animator = {
50 |
51 | // create the animator with the duration and timing curve
52 | // (in this case using a spring-physics)
53 | let animator = UIViewPropertyAnimator(duration: 2.0, dampingRatio: 0.5)
54 |
55 | // define the properties to animate
56 | animator.addAnimations {
57 | square.transform = CGAffineTransform(rotationAngle: CGFloat.pi/2)
58 | square.bounds.size = CGSize(width: 50, height: 150)
59 | square.backgroundColor = .blue
60 | }
61 | // return the animator
62 | return animator
63 | }
64 | ````
65 |
66 | `ScrubContainerView` adds a `UISlider` that lets you scrub through the animation
67 | and look at it at any intermediate step.
68 |
69 |
70 |
71 | An option for step #3 is to wrap the expressions that define the stating state
72 | into the `startState` closure:
73 |
74 | ````Swift
75 | container.startState = {
76 | square.transform = .identity
77 | square.bounds.size = CGSize(width: 150, height: 50)
78 | square.backgroundColor = .red
79 | }
80 | ````
81 |
82 | If this property is defined `ScrubContainerView` will add a button button that
83 | lets you to watch the animation perform with it's defined duration and timing
84 | curve:
85 |
86 |
87 |
88 | ### Initializers
89 |
90 | A `ScrubContainerView` can be initialized multiple ways. The default initializer
91 | creates a stage width a 300x300 pixel size.
92 |
93 | `ScrubContainerView.init(width: Double, height: Double)` allows a stage of any size.
94 |
95 | Convenience initializers `ScrubContainerView.init(device: Device)` and
96 | `ScrubContainerView.init(device: Device, orientation: Orientation)` allow for
97 | stages to be created for `Device.iPhoneSE`, `.iPhone`, and .`iPhonePlus` and
98 | orientations `.portrait` or `.landscape`.
99 |
100 | ### Examples
101 |
102 | The playground includes the example above, as well as a [slightly more complex
103 | example](https://github.com/mathewsanders/Scrubber/blob/master/source/Animation.playground/Pages/Keyframe%20example.xcplaygroundpage/Contents.swift)
104 | that includes a 4-step animation chained together using keyframes to show how
105 | more complex multi-step animations can be built, and a blank template ready to
106 | start writing a new animation from scratch.
107 |
108 |
109 |
110 | ## Roadmap
111 | - [x] UISlider to scrub animations
112 | - [x] Button to play animation with default timing curve and duration
113 | - [x] Initializers for different device sizes
114 | - [ ] Resume animation after scrubbing
115 | - [ ] Pause/resume animation
116 | - [ ] Scrub paused animation
117 |
118 | ## Requirements
119 |
120 | - Swift 3.0
121 | - iOS/tvOS 10.0+
122 |
123 | ## Author
124 |
125 | Made with :heart: by [@permakittens](http://twitter.com/permakittens)
126 |
127 | ## Contributing
128 |
129 | Feedback, or contributions for bug fixing or improvements are welcome. Feel free to submit a pull request or open an issue.
130 |
131 | ## License
132 |
133 | MIT
134 |
--------------------------------------------------------------------------------
/assets/playground-simple.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathewsanders/Scrubber/71b148fe33528d9321f9de8417b0dc9e819dc40d/assets/playground-simple.gif
--------------------------------------------------------------------------------
/assets/scrubber-chained-1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathewsanders/Scrubber/71b148fe33528d9321f9de8417b0dc9e819dc40d/assets/scrubber-chained-1.gif
--------------------------------------------------------------------------------
/assets/scrubber-simple-1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathewsanders/Scrubber/71b148fe33528d9321f9de8417b0dc9e819dc40d/assets/scrubber-simple-1.gif
--------------------------------------------------------------------------------
/assets/scrubber-simple-2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mathewsanders/Scrubber/71b148fe33528d9321f9de8417b0dc9e819dc40d/assets/scrubber-simple-2.gif
--------------------------------------------------------------------------------
/source/Animation.playground/Pages/Keyframe example.xcplaygroundpage/Contents.swift:
--------------------------------------------------------------------------------
1 | //: [Previous](@previous)
2 |
3 | //: Keyframe animation example
4 |
5 | import UIKit
6 | import PlaygroundSupport
7 |
8 | let container = ScrubContainerView(device: .iPhoneSE)
9 | PlaygroundPage.current.liveView = container
10 |
11 | // some useful variables to help with animations
12 | let offstageLeft = CGAffineTransform(translationX: -100, y: 0)
13 | let offstageRight = CGAffineTransform(translationX: 100, y: 0)
14 | let capsuleFrameWide = CGRect(origin: .zero, size: CGSize(width: 80, height: 40))
15 | let capsuleFrameNarrow = CGRect(origin: .zero, size: CGSize(width: 40, height: 40))
16 |
17 | // create view to hold the dots ('capsule')
18 | let capsule = UIView(frame: capsuleFrameWide)
19 | capsule.backgroundColor = #colorLiteral(red: 0.921431005, green: 0.9214526415, blue: 0.9214410186, alpha: 1)
20 | capsule.layer.cornerRadius = 20
21 | capsule.clipsToBounds = true
22 | container.stage.addSubview(capsule)
23 |
24 | // create three dots and position relative to capsule
25 | let capsuleDots = [UIView(), UIView(), UIView()]
26 |
27 | capsuleDots.forEach({ dot in
28 | capsule.addSubview(dot)
29 | dot.center = capsule.center
30 | dot.bounds.size = CGSize(width: 10, height: 10)
31 | dot.layer.cornerRadius = 5
32 | dot.backgroundColor = #colorLiteral(red: 0.2196078449, green: 0.007843137719, blue: 0.8549019694, alpha: 1)
33 | })
34 |
35 | capsuleDots[0].center = capsule.center.applying(CGAffineTransform(translationX: 20, y: 0))
36 | capsuleDots[1].center = capsule.center
37 | capsuleDots[2].center = capsule.center.applying(CGAffineTransform(translationX: -20, y: 0))
38 |
39 | capsule.center = container.stage.center
40 |
41 | // reset the dots and capsule for animation start
42 | container.startState = {
43 |
44 | capsuleDots.forEach({ dot in
45 | dot.transform = offstageLeft
46 | })
47 |
48 | capsule.bounds = capsuleFrameNarrow
49 | }
50 |
51 | // define the four steps of the animation
52 | container.animator = {
53 |
54 | let animator = UIViewPropertyAnimator(duration: 1.5, curve: .easeInOut)
55 |
56 | animator.addAnimations {
57 |
58 | UIView.animateKeyframes(withDuration: 2, delay: 0, options: [.calculationModeLinear], animations: {
59 |
60 | // Step 1: make the capsule grow to large size
61 | UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.1) {
62 | capsule.bounds = capsuleFrameWide
63 | }
64 |
65 | // enumerate over dots so they are slightly staggered
66 | for (index, dot) in capsuleDots.enumerated() {
67 | let offsetDelay = TimeInterval(index) * 0.025
68 |
69 | // Step 2: move the dots to their default positions, and fade in
70 | UIView.addKeyframe(withRelativeStartTime: 0.05 + offsetDelay, relativeDuration: 0.2) {
71 | dot.transform = .identity
72 | dot.alpha = 1.0
73 | }
74 |
75 | // Step 3: fade out dots and translate to the right
76 | UIView.addKeyframe(withRelativeStartTime: 0.8 + offsetDelay, relativeDuration: 0.2) {
77 |
78 | //dot.alpha = 0.0
79 | dot.transform = offstageRight
80 | }
81 | }
82 |
83 | // Step 4: make capsure move to narrow width
84 | UIView.addKeyframe(withRelativeStartTime: 0.875, relativeDuration: 0.1) {
85 | capsule.bounds = capsuleFrameNarrow
86 | }
87 | })
88 | }
89 |
90 | return animator
91 | }
92 |
93 | //: [Next](@next)
94 |
--------------------------------------------------------------------------------
/source/Animation.playground/Pages/Simple example.xcplaygroundpage/Contents.swift:
--------------------------------------------------------------------------------
1 | //: Simple animation example
2 |
3 | import UIKit
4 | import PlaygroundSupport
5 |
6 | let container = ScrubContainerView()
7 | PlaygroundPage.current.liveView = container
8 |
9 | // set up views to animate
10 | let square = UIView()
11 | container.stage.addSubview(square)
12 | square.center = container.stage.center
13 |
14 | container.startState = {
15 | square.transform = .identity
16 | square.bounds.size = CGSize(width: 150, height: 50)
17 | square.backgroundColor = .red
18 | }
19 |
20 | // provide the container the animator to scrub
21 | container.animator = {
22 |
23 | let animator = UIViewPropertyAnimator(duration: 2.0, dampingRatio: 0.5)
24 |
25 | animator.addAnimations {
26 | square.transform = CGAffineTransform(rotationAngle: CGFloat.pi/2)
27 | square.bounds.size = CGSize(width: 50, height: 150)
28 | square.backgroundColor = .blue
29 | }
30 |
31 | return animator
32 | }
33 |
--------------------------------------------------------------------------------
/source/Animation.playground/Pages/Template.xcplaygroundpage/Contents.swift:
--------------------------------------------------------------------------------
1 | //: Simple animation example
2 |
3 | import UIKit
4 | import PlaygroundSupport
5 |
6 | let container = ScrubContainerView()
7 | PlaygroundPage.current.liveView = container
8 |
9 | // create objects and add to `container.stage`
10 |
11 | // define the animation start/reset state
12 | container.startState = {
13 |
14 | }
15 |
16 | container.animator = {
17 |
18 | // set up the animator
19 | let animator = UIViewPropertyAnimator(duration: 1.0, curve: .easeInOut)
20 |
21 | // define the animations
22 | animator.addAnimations {
23 |
24 | }
25 |
26 | return animator
27 | }
28 |
--------------------------------------------------------------------------------
/source/Animation.playground/Sources/ScrubContainerView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public class ScrubContainerView: UIView {
4 |
5 | private let scrubber: UISlider
6 | private let playButton: UIButton
7 | private let resetButton: UIButton
8 | private var scrubAnimator: UIViewPropertyAnimator?
9 | private var containerView = UIView()
10 | private let stack = UIStackView()
11 |
12 | public var stage: UIView {
13 | return containerView
14 | }
15 |
16 | /// The property animator to drive animations
17 | public var animator: (() -> UIViewPropertyAnimator)? {
18 | didSet {
19 | scrubAnimator = animator?()
20 | }
21 | }
22 |
23 | /// The start state for objects to be animated
24 | public var startState: (() -> ())? {
25 | didSet {
26 | guard let startState = startState else { return }
27 |
28 | startState()
29 | showButtons()
30 | }
31 | }
32 |
33 | public enum Device {
34 | case iPhone
35 | case iPhonePlus
36 | case iPhoneSE
37 |
38 | var portraitSize: CGSize {
39 | switch self {
40 | case .iPhoneSE: return CGSize(width: 320, height: 568)
41 | case .iPhone: return CGSize(width: 375, height: 667)
42 | case .iPhonePlus: return CGSize(width: 414, height: 736)
43 | }
44 | }
45 |
46 | func size(for orientation: Orientation) -> CGSize {
47 | switch orientation {
48 | case .portrait: return portraitSize
49 | case .landscape: return CGSize(width: portraitSize.height, height: portraitSize.width)
50 | }
51 | }
52 | }
53 |
54 | public enum Orientation {
55 | case portrait
56 | case landscape
57 | }
58 |
59 | public convenience init(width: Double, height: Double) {
60 | self.init(size: CGSize(width: width, height: height))
61 | }
62 |
63 | public convenience init(device: Device, orientation: Orientation = .portrait) {
64 | self.init(size: device.size(for: orientation))
65 | }
66 |
67 | public init(size: CGSize = CGSize(width: 300, height: 300)) {
68 |
69 | containerView.frame = CGRect(origin: .zero, size: size)
70 | containerView.backgroundColor = .white
71 |
72 | playButton = UIButton(type: .system)
73 | resetButton = UIButton(type: .system)
74 | scrubber = UISlider()
75 |
76 | super.init(frame: CGRect(origin: .zero, size: CGSize(width: size.width + 20, height: size.height + 60)))
77 |
78 | backgroundColor = UIColor.groupTableViewBackground
79 | containerView.layer.cornerRadius = 4
80 | addSubview(containerView)
81 |
82 | containerView.transform = CGAffineTransform(translationX: 10, y: 10)
83 |
84 | playButton.isHidden = true
85 | resetButton.isHidden = true
86 |
87 | stack.alignment = .fill
88 | stack.axis = .horizontal
89 | stack.distribution = .fill
90 | stack.spacing = 10
91 | stack.translatesAutoresizingMaskIntoConstraints = false
92 | addSubview(stack)
93 |
94 | stack.leftAnchor.constraint(equalTo: leftAnchor, constant: 10).isActive = true
95 | stack.rightAnchor.constraint(equalTo: rightAnchor, constant: -10).isActive = true
96 | stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 0).isActive = true
97 | stack.heightAnchor.constraint(equalToConstant: 50).isActive = true
98 |
99 | stack.addArrangedSubview(playButton)
100 | stack.addArrangedSubview(scrubber)
101 | stack.addArrangedSubview(resetButton)
102 |
103 |
104 | scrubber.addTarget(self, action: #selector(ScrubContainerView.handleScrub), for: .valueChanged)
105 | }
106 |
107 | public override func addSubview(_ view: UIView) {
108 | if view == containerView || view == stack {
109 | super.addSubview(view)
110 | }
111 | else {
112 | print("adding", view, "to contaier")
113 | containerView.addSubview(view)
114 | }
115 | }
116 |
117 | private func showButtons() {
118 |
119 | playButton.setTitle("Play", for: .normal)
120 | resetButton.setTitle("Reset", for: .normal)
121 |
122 | playButton.addTarget(self, action: #selector(ScrubContainerView.handlePlay), for: .touchDown)
123 | resetButton.addTarget(self, action: #selector(ScrubContainerView.handleReset), for: .touchDown)
124 |
125 | playButton.isHidden = false
126 | resetButton.isHidden = false
127 | }
128 |
129 | func handlePlay() {
130 |
131 | guard let animator = animator?() else { return }
132 |
133 | animator.addAnimations {
134 | self.scrubber.setValue(1.0, animated: true)
135 | }
136 |
137 | animator.addCompletion({ _ in
138 | self.handleReset()
139 | })
140 |
141 | playButton.isEnabled = false
142 | scrubber.isEnabled = false
143 |
144 | animator.startAnimation()
145 | }
146 |
147 | func handleReset() {
148 |
149 | scrubber.isEnabled = true
150 | playButton.isEnabled = true
151 |
152 | scrubAnimator?.stopAnimation(true)
153 | scrubAnimator = animator?()
154 | scrubber.setValue(0.0, animated: true)
155 | startState?()
156 | }
157 |
158 | func handleScrub() {
159 | playButton.isEnabled = false
160 | scrubAnimator?.fractionComplete = CGFloat(scrubber.value)
161 | }
162 |
163 | required public init?(coder aDecoder: NSCoder) {
164 | fatalError("init(coder:) has not been implemented")
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/source/Animation.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------