Skip to content

Commit 962c919

Browse files
author
Mark Pospesel
authored
[Issue 58] Add Animation struct (#59)
1 parent cea9def commit 962c919

File tree

5 files changed

+293
-0
lines changed

5 files changed

+293
-0
lines changed

Diff for: README.md

+54
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,60 @@ scrollView.registerKeyboardNotifications()
394394
💡 Almost every full-screen view in your app that contains any text should be a vertical scroll view because of the vagaries of localization, Dynamic Type, potentially small screen sizes, and landscape mode support.
395395
</aside>
396396

397+
### 5. Other
398+
399+
#### Elevation
400+
401+
`Elevation` is a model object to define shadows similarly to [W3C box-shadows](https://www.w3.org/TR/css-backgrounds-3/#box-shadow) and [Figma drop shadows](https://help.figma.com/hc/en-us/articles/360041488473-Apply-shadow-or-blur-effects#shadow). It has the following parameters that match how Figma (and web) define drop shadows:
402+
403+
* offset (x and y)
404+
* blur
405+
* spread
406+
* color
407+
408+
`Elevation` has an `apply` method that then applies that shadow effect to a `CALayer`. Remember to call it every time your color mode changes to update the shadow color (a `CGColor`).
409+
410+
```swift
411+
let button = UIButton()
412+
let elevation = Elevation(
413+
xOffset: 0,
414+
yOffset: 2,
415+
blur: 5,
416+
spread: 0,
417+
color: .black,
418+
opacity: 0.5
419+
)
420+
elevation.apply(layer: button.layer, cornerRadius: 8)
421+
```
422+
423+
#### Animation
424+
425+
`Animation` is a model object to define UIView animations. It has the following parameters:
426+
427+
* duration
428+
* delay
429+
* curve
430+
431+
`Animation.curve` is an enum with associated values that can be either `.regular` or `.spring`.
432+
433+
There is a `UIView` class override method for `animate` that takes an `Animation` object.
434+
435+
The advantage of adopting the `Animation` structure is that with a single method you can animate either a regular or spring animation. This allows us to build components where the user can customize the animations used without having our code be overly complex or fragile.
436+
437+
```swift
438+
let button = UIButton()
439+
button.alpha = 1
440+
let animation = Animation(duration: 0.25, curve: .regular(options: .curveEaseOut))
441+
442+
UIView.animate(with: animation) {
443+
// fade button out
444+
button.alpha = 0
445+
} completion: {
446+
// remove it from the superview when done
447+
button.removeFromSuperview()
448+
}
449+
```
450+
397451
Installation
398452
----------
399453

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// UIView+Animation.swift
3+
// YCoreUI
4+
//
5+
// Created by Mark Pospesel on 3/31/23.
6+
// Copyright © 2023 Y Media Labs. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
/// Adds support for executing animations using properties from `Animation`
12+
public extension UIView {
13+
/// Executes an animation using the specified parameters
14+
/// - Parameters:
15+
/// - parameters: specifies duration, delay, curve type and options
16+
/// - animations: the animation block to perform
17+
/// - completion: the optional completion block to be called when the animation completes
18+
class func animate(
19+
with parameters: Animation,
20+
animations: @escaping () -> Void,
21+
completion: ((Bool) -> Void)? = nil
22+
) {
23+
switch parameters.curve {
24+
case .regular(options: let options):
25+
animate(
26+
withDuration: parameters.duration,
27+
delay: parameters.delay,
28+
options: options,
29+
animations: animations,
30+
completion: completion
31+
)
32+
case .spring(damping: let damping, velocity: let velocity, let options):
33+
animate(
34+
withDuration: parameters.duration,
35+
delay: parameters.delay,
36+
usingSpringWithDamping: damping,
37+
initialSpringVelocity: velocity,
38+
options: options,
39+
animations: animations,
40+
completion: completion
41+
)
42+
}
43+
}
44+
}

Diff for: Sources/YCoreUI/Foundations/Animation.swift

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// Animation.swift
3+
// YCoreUI
4+
//
5+
// Created by Mark Pospesel on 3/30/23.
6+
// Copyright © 2023 Y Media Labs. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
/// Specifies the parameters to perform animations.
12+
///
13+
/// To be used with `UIView.animate(parameters:animations:completion:)`.
14+
public struct Animation: Equatable {
15+
/// Animation curve
16+
public enum Curve: Equatable {
17+
/// Regular animation curve
18+
case regular(options: UIView.AnimationOptions)
19+
/// Spring animation
20+
case spring(damping: CGFloat, velocity: CGFloat, options: UIView.AnimationOptions = [])
21+
}
22+
23+
/// Duration of the animation (in seconds). Defaults to `0.3`.
24+
public var duration: TimeInterval
25+
26+
/// Delay of the animation (in seconds). Defaults to `0.0`.
27+
public var delay: TimeInterval
28+
29+
/// Animation curve to apply. Defaults to `.regular(options: .curveEaseInOut)`.
30+
public var curve: Curve
31+
32+
/// Creates animation parameters
33+
/// - Parameters:
34+
/// - duration: duration of the animation
35+
/// - delay: delay of the animation
36+
/// - curve: animation curve to apply
37+
public init(
38+
duration: TimeInterval = 0.3,
39+
delay: TimeInterval = 0.0,
40+
curve: Curve = .regular(options: .curveEaseInOut)
41+
) {
42+
self.duration = duration
43+
self.delay = delay
44+
self.curve = curve
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//
2+
// UIView+AnimationTests.swift
3+
// YCoreUI
4+
//
5+
// Created by Mark Pospesel on 3/31/23.
6+
// Copyright © 2023 Y Media Labs. All rights reserved.
7+
//
8+
9+
import XCTest
10+
@testable import YCoreUI
11+
12+
final class UIViewAnimationTests: XCTestCase {
13+
func test_regular_deliversAnimation() throws {
14+
defer { SpyView.reset() }
15+
// Given
16+
let duration = CGFloat(Int.random(in: 1...5)) / 10.0
17+
let delay = CGFloat(Int.random(in: 1...5)) / 10.0
18+
let options = try XCTUnwrap(getOptions().randomElement())
19+
let sut = Animation(
20+
duration: duration,
21+
delay: delay,
22+
curve: .regular(options: options)
23+
)
24+
var isAnimationBlockCalled = false
25+
var isCompletionBlockCalled = false
26+
27+
// When
28+
SpyView.animate(
29+
with: sut
30+
) {
31+
isAnimationBlockCalled = true
32+
} completion: { _ in
33+
isCompletionBlockCalled = true
34+
}
35+
36+
// Then
37+
XCTAssertEqual(SpyView.lastAnimation, sut)
38+
XCTAssertTrue(isAnimationBlockCalled)
39+
XCTAssertTrue(isCompletionBlockCalled)
40+
}
41+
42+
func test_spring_deliversAnimation() throws {
43+
defer { SpyView.reset() }
44+
// Given
45+
let duration = CGFloat(Int.random(in: 1...5)) / 10.0
46+
let delay = CGFloat(Int.random(in: 1...5)) / 10.0
47+
let options = try XCTUnwrap(getOptions().randomElement())
48+
let damping = CGFloat(Int.random(in: 1...10)) / 10.0
49+
let velocity = CGFloat(Int.random(in: 1...6)) / 10.0
50+
let sut = Animation(
51+
duration: duration,
52+
delay: delay,
53+
curve: .spring(damping: damping, velocity: velocity, options: options)
54+
)
55+
var isAnimationBlockCalled = false
56+
var isCompletionBlockCalled = false
57+
58+
// When
59+
SpyView.animate(
60+
with: sut
61+
) {
62+
isAnimationBlockCalled = true
63+
} completion: { _ in
64+
isCompletionBlockCalled = true
65+
}
66+
67+
// Then
68+
XCTAssertEqual(SpyView.lastAnimation, sut)
69+
XCTAssertTrue(isAnimationBlockCalled)
70+
XCTAssertTrue(isCompletionBlockCalled)
71+
}
72+
}
73+
74+
extension UIViewAnimationTests {
75+
func getOptions() -> [UIView.AnimationOptions] {
76+
[
77+
[],
78+
.curveEaseIn,
79+
.curveEaseInOut,
80+
.curveEaseOut,
81+
.beginFromCurrentState
82+
]
83+
}
84+
}
85+
86+
final class SpyView: UIView {
87+
static var lastAnimation: Animation?
88+
89+
override class func animate(
90+
withDuration duration: TimeInterval,
91+
delay: TimeInterval,
92+
options: UIView.AnimationOptions = [],
93+
animations: @escaping () -> Void,
94+
completion: ((Bool) -> Void)? = nil
95+
) {
96+
lastAnimation = Animation(
97+
duration: duration,
98+
delay: delay,
99+
curve: .regular(options: options)
100+
)
101+
animations()
102+
completion?(true)
103+
}
104+
105+
override class func animate(
106+
withDuration duration: TimeInterval,
107+
delay: TimeInterval,
108+
usingSpringWithDamping
109+
dampingRatio: CGFloat,
110+
initialSpringVelocity velocity: CGFloat,
111+
options: UIView.AnimationOptions = [],
112+
animations: @escaping () -> Void,
113+
completion: ((Bool) -> Void)? = nil
114+
) {
115+
lastAnimation = Animation(
116+
duration: duration,
117+
delay: delay,
118+
curve: .spring(damping: dampingRatio, velocity: velocity, options: options)
119+
)
120+
animations()
121+
completion?(true)
122+
}
123+
124+
class func reset() {
125+
lastAnimation = nil
126+
}
127+
}

Diff for: Tests/YCoreUITests/Foundations/AnimationTests.swift

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// AnimationTests.swift
3+
// YCoreUI
4+
//
5+
// Created by Mark Pospesel on 3/30/23.
6+
// Copyright © 2023 Y Media Labs. All rights reserved.
7+
//
8+
9+
import XCTest
10+
@testable import YCoreUI
11+
12+
final class AnimationTests: XCTestCase {
13+
func test_defaults() {
14+
// Given
15+
let sut = Animation()
16+
17+
// Then
18+
XCTAssertEqual(sut.duration, 0.3)
19+
XCTAssertEqual(sut.delay, 0.0)
20+
XCTAssertEqual(sut.curve, .regular(options: .curveEaseInOut))
21+
}
22+
}

0 commit comments

Comments
 (0)