Skip to content

Commit a8a9679

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 a8a9679

File tree

1 file changed

+26
-24
lines changed

1 file changed

+26
-24
lines changed

FluidInterfaces/FluidInterfaces/Spring.swift

+26-24
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ class SpringInterfaceViewController: InterfaceViewController {
2020

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

3333
private lazy var frequencySliderView: SliderView = {
3434
let sliderView = SliderView()
35+
sliderView.valueFormatter = { String(format: "%.2fs", $0) }
3536
sliderView.translatesAutoresizingMaskIntoConstraints = false
36-
sliderView.title = "RESPONSE (SPEED)"
37-
sliderView.minValue = 0.1
38-
sliderView.maxValue = 2
37+
sliderView.title = "RESPONSE"
38+
sliderView.range = 0.1...2
3939
sliderView.value = frequencyResponse
4040
sliderView.sliderMovedAction = { self.frequencyResponse = $0 }
4141
sliderView.sliderFinishedMovingAction = { self.resetAnimation() }
@@ -46,6 +46,8 @@ class SpringInterfaceViewController: InterfaceViewController {
4646
private var frequencyResponse: CGFloat = 1
4747

4848
private let margin: CGFloat = 30
49+
50+
private var leadingAnchor, trailingAnchor : NSLayoutConstraint!
4951

5052
override func viewDidLoad() {
5153
super.viewDidLoad()
@@ -63,7 +65,10 @@ class SpringInterfaceViewController: InterfaceViewController {
6365
view.addSubview(springView)
6466
springView.heightAnchor.constraint(equalToConstant: 80).isActive = true
6567
springView.widthAnchor.constraint(equalToConstant: 80).isActive = true
66-
springView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin).isActive = true
68+
self.leadingAnchor = springView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin)
69+
self.leadingAnchor.isActive = true
70+
self.trailingAnchor = springView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin)
71+
self.trailingAnchor.isActive = false
6772
springView.bottomAnchor.constraint(equalTo: dampingSliderView.topAnchor, constant: -80).isActive = true
6873

6974
animateView()
@@ -74,22 +79,21 @@ class SpringInterfaceViewController: InterfaceViewController {
7479

7580
/// Repeatedly animates the view using the current `dampingRatio` and `frequencyResponse`.
7681
private func animateView() {
82+
self.view.layoutIfNeeded()
83+
7784
let timingParameters = UISpringTimingParameters(damping: dampingRatio, response: frequencyResponse)
7885
animator = UIViewPropertyAnimator(duration: 0, timingParameters: timingParameters)
7986
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()
87+
self.leadingAnchor.isActive = !self.leadingAnchor.isActive
88+
self.trailingAnchor.isActive = !self.trailingAnchor.isActive
89+
self.view.layoutIfNeeded()
8690
}
91+
animator.addCompletion { _ in self.animateView() }
8792
animator.startAnimation()
8893
}
8994

9095
private func resetAnimation() {
9196
animator.stopAnimation(true)
92-
self.springView.transform = .identity
9397
animateView()
9498
}
9599

@@ -110,22 +114,20 @@ class SliderView: UIView {
110114
}
111115
set {
112116
slider.value = Float(newValue)
113-
valueLabel.text = String(format: "%.2f", newValue)
114-
}
115-
}
116-
117-
public var minValue: CGFloat = 0 {
118-
didSet {
119-
slider.minimumValue = Float(minValue)
117+
valueLabel.text = valueFormatter(newValue)
120118
}
121119
}
122120

123-
public var maxValue: CGFloat = 1 {
121+
public var range: ClosedRange<Float> = 0...1 {
124122
didSet {
125-
slider.maximumValue = Float(maxValue)
123+
slider.minimumValue = range.lowerBound
124+
slider.maximumValue = range.upperBound
126125
}
127126
}
128127

128+
/// Code to format the value to a string for the valueLabel
129+
public var valueFormatter: (CGFloat) -> (String) = { String(format: "%.2f", $0) }
130+
129131
/// Code that's executed when the slider moves.
130132
public var sliderMovedAction: (CGFloat) -> () = { _ in }
131133

@@ -185,7 +187,7 @@ class SliderView: UIView {
185187
}
186188

187189
@objc private func sliderMoved(slider: UISlider, event: UIEvent) {
188-
valueLabel.text = String(format: "%.2f", slider.value)
190+
valueLabel.text = valueFormatter(CGFloat(slider.value))
189191
sliderMovedAction(CGFloat(slider.value))
190192
if event.allTouches?.first?.phase == .ended { sliderFinishedMovingAction() }
191193
}

0 commit comments

Comments
 (0)