Skip to content

Commit 9cc4420

Browse files
committed
Improve Spring Animation example
* Continuously animate between two states (left and right) using layout constraints to avoid the jumpiness of resetting the position * Removing terms bounciness and speed (these are confusing because their ranges are opposite: low damping = high bounciness, short response = high speed) * Make value labels more clear by adding units (50% damping, 1s response instead of 0.5 and 1.0)
1 parent 1701098 commit 9cc4420

File tree

1 file changed

+42
-36
lines changed

1 file changed

+42
-36
lines changed

FluidInterfaces/FluidInterfaces/Spring.swift

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ class SpringInterfaceViewController: InterfaceViewController {
2020

2121
private lazy var dampingSliderView: SliderView = {
2222
let sliderView = SliderView()
23-
sliderView.translatesAutoresizingMaskIntoConstraints = false
24-
sliderView.title = "DAMPING (BOUNCINESS)"
25-
sliderView.minValue = 0.1
26-
sliderView.maxValue = 1
23+
sliderView.valueFormatter = { String(format: "%i%%", Int($0 * 100)) }
24+
sliderView.title = "DAMPING"
25+
sliderView.range = 0.1...1
2726
sliderView.value = dampingRatio
2827
sliderView.sliderMovedAction = { self.dampingRatio = $0 }
2928
sliderView.sliderFinishedMovingAction = { self.resetAnimation() }
@@ -32,10 +31,9 @@ class SpringInterfaceViewController: InterfaceViewController {
3231

3332
private lazy var frequencySliderView: SliderView = {
3433
let sliderView = SliderView()
35-
sliderView.translatesAutoresizingMaskIntoConstraints = false
36-
sliderView.title = "RESPONSE (SPEED)"
37-
sliderView.minValue = 0.1
38-
sliderView.maxValue = 2
34+
sliderView.valueFormatter = { String(format: "%.2fs", $0) }
35+
sliderView.title = "RESPONSE"
36+
sliderView.range = 0.1...2
3937
sliderView.value = frequencyResponse
4038
sliderView.sliderMovedAction = { self.frequencyResponse = $0 }
4139
sliderView.sliderFinishedMovingAction = { self.resetAnimation() }
@@ -46,26 +44,37 @@ class SpringInterfaceViewController: InterfaceViewController {
4644
private var frequencyResponse: CGFloat = 1
4745

4846
private let margin: CGFloat = 30
47+
48+
private var leadingAnchor, trailingAnchor : NSLayoutConstraint!
4949

5050
override func viewDidLoad() {
5151
super.viewDidLoad()
5252

5353
view.addSubview(dampingSliderView)
54-
dampingSliderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin).isActive = true
55-
dampingSliderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin).isActive = true
56-
dampingSliderView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true
54+
UIView.activate(constraints: [
55+
dampingSliderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin),
56+
dampingSliderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin),
57+
dampingSliderView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0)
58+
])
5759

5860
view.addSubview(frequencySliderView)
59-
frequencySliderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin).isActive = true
60-
frequencySliderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin).isActive = true
61-
frequencySliderView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 140).isActive = true
61+
UIView.activate(constraints: [
62+
frequencySliderView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin),
63+
frequencySliderView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin),
64+
frequencySliderView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 140)
65+
])
6266

6367
view.addSubview(springView)
64-
springView.heightAnchor.constraint(equalToConstant: 80).isActive = true
65-
springView.widthAnchor.constraint(equalToConstant: 80).isActive = true
66-
springView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin).isActive = true
67-
springView.bottomAnchor.constraint(equalTo: dampingSliderView.topAnchor, constant: -80).isActive = true
68-
68+
UIView.activate(constraints: [
69+
springView.heightAnchor.constraint(equalToConstant: 80),
70+
springView.widthAnchor.constraint(equalToConstant: 80),
71+
springView.bottomAnchor.constraint(equalTo: dampingSliderView.topAnchor, constant: -80)
72+
])
73+
self.leadingAnchor = springView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin)
74+
self.leadingAnchor.isActive = true
75+
self.trailingAnchor = springView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin)
76+
self.trailingAnchor.isActive = false
77+
6978
animateView()
7079

7180
}
@@ -74,22 +83,21 @@ class SpringInterfaceViewController: InterfaceViewController {
7483

7584
/// Repeatedly animates the view using the current `dampingRatio` and `frequencyResponse`.
7685
private func animateView() {
86+
self.view.layoutIfNeeded()
87+
7788
let timingParameters = UISpringTimingParameters(damping: dampingRatio, response: frequencyResponse)
7889
animator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters)
7990
animator.addAnimations {
80-
let translation = self.view.bounds.width - 2 * self.margin - 80
81-
self.springView.transform = CGAffineTransform(translationX: translation, y: 0)
82-
}
83-
animator.addCompletion { _ in
84-
self.springView.transform = .identity
85-
self.animateView()
91+
self.leadingAnchor.isActive = !self.leadingAnchor.isActive
92+
self.trailingAnchor.isActive = !self.trailingAnchor.isActive
93+
self.view.layoutIfNeeded()
8694
}
95+
animator.addCompletion { _ in self.animateView() }
8796
animator.startAnimation()
8897
}
8998

9099
private func resetAnimation() {
91100
animator.stopAnimation(true)
92-
self.springView.transform = .identity
93101
animateView()
94102
}
95103

@@ -110,22 +118,20 @@ class SliderView: UIView {
110118
}
111119
set {
112120
slider.value = Float(newValue)
113-
valueLabel.text = String(format: "%.2f", newValue)
121+
valueLabel.text = valueFormatter(newValue)
114122
}
115123
}
116124

117-
public var minValue: CGFloat = 0 {
125+
public var range: ClosedRange<Float> = 0...1 {
118126
didSet {
119-
slider.minimumValue = Float(minValue)
120-
}
121-
}
122-
123-
public var maxValue: CGFloat = 1 {
124-
didSet {
125-
slider.maximumValue = Float(maxValue)
127+
slider.minimumValue = range.lowerBound
128+
slider.maximumValue = range.upperBound
126129
}
127130
}
128131

132+
/// Code to format the value to a string for the valueLabel
133+
public var valueFormatter: (CGFloat) -> (String) = { String(format: "%.2f", $0) }
134+
129135
/// Code that's executed when the slider moves.
130136
public var sliderMovedAction: (CGFloat) -> () = { _ in }
131137

@@ -185,7 +191,7 @@ class SliderView: UIView {
185191
}
186192

187193
@objc private func sliderMoved(slider: UISlider, event: UIEvent) {
188-
valueLabel.text = String(format: "%.2f", slider.value)
194+
valueLabel.text = valueFormatter(CGFloat(slider.value))
189195
sliderMovedAction(CGFloat(slider.value))
190196
if event.allTouches?.first?.phase == .ended { sliderFinishedMovingAction() }
191197
}

0 commit comments

Comments
 (0)