Skip to content

Commit 7164d6b

Browse files
authored
Mirror Transitions (#24)
Closes #7. Implementation was a breeze thanks to the work in #6.
1 parent d6c3af3 commit 7164d6b

File tree

21 files changed

+267
-103
lines changed

21 files changed

+267
-103
lines changed

Demo/Demo.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
D5535843290F4BEA009E5D72 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5535842290F4BEA009E5D72 /* AppView.swift */; };
1919
D5535845290F52F7009E5D72 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5535844290F52F7009E5D72 /* SettingsView.swift */; };
2020
D5535847290F5E6F009E5D72 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5535846290F5E6F009E5D72 /* AppState.swift */; };
21+
D5755A79291ADC00007F2201 /* Zoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5755A78291ADC00007F2201 /* Zoom.swift */; };
2122
D5AAF4052911C59E009743D3 /* PageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF4042911C59E009743D3 /* PageView.swift */; };
2223
D5AAF4072911C621009743D3 /* Pages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF4062911C621009743D3 /* Pages.swift */; };
2324
/* End PBXBuildFile section */
@@ -35,6 +36,7 @@
3536
D5535842290F4BEA009E5D72 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
3637
D5535844290F52F7009E5D72 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
3738
D5535846290F5E6F009E5D72 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
39+
D5755A78291ADC00007F2201 /* Zoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zoom.swift; sourceTree = "<group>"; };
3840
D5AAF4042911C59E009743D3 /* PageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageView.swift; sourceTree = "<group>"; };
3941
D5AAF4062911C621009743D3 /* Pages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pages.swift; sourceTree = "<group>"; };
4042
/* End PBXFileReference section */
@@ -82,6 +84,7 @@
8284
D5AAF4042911C59E009743D3 /* PageView.swift */,
8385
D5535844290F52F7009E5D72 /* SettingsView.swift */,
8486
D553582C290E9718009E5D72 /* Swing.swift */,
87+
D5755A78291ADC00007F2201 /* Zoom.swift */,
8588
D5535822290E9692009E5D72 /* Assets.xcassets */,
8689
D5535824290E9692009E5D72 /* Preview Content */,
8790
);
@@ -180,6 +183,7 @@
180183
files = (
181184
D5AAF4072911C621009743D3 /* Pages.swift in Sources */,
182185
D5535845290F52F7009E5D72 /* SettingsView.swift in Sources */,
186+
D5755A79291ADC00007F2201 /* Zoom.swift in Sources */,
183187
D5AAF4052911C59E009743D3 /* PageView.swift in Sources */,
184188
D5535839290E9718009E5D72 /* AppDelegate.swift in Sources */,
185189
D5535847290F5E6F009E5D72 /* AppState.swift in Sources */,

Demo/Demo/AppState.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ final class AppState: ObservableObject {
99
case slideAndFade
1010
case moveVertically
1111
case swing
12+
case zoom
1213

1314
var description: String {
1415
switch self {
@@ -24,6 +25,8 @@ final class AppState: ObservableObject {
2425
return "Slide Vertically"
2526
case .swing:
2627
return "Swing"
28+
case .zoom:
29+
return "Zoom"
2730
}
2831
}
2932

@@ -41,6 +44,8 @@ final class AppState: ObservableObject {
4144
return .slide(axis: .vertical)
4245
case .swing:
4346
return .swing
47+
case .zoom:
48+
return .zoom
4449
}
4550
}
4651
}

Demo/Demo/Swing.swift

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,41 +9,25 @@ extension AnyNavigationTransition {
99

1010
struct Swing: NavigationTransition {
1111
var body: some NavigationTransition {
12-
let angle = Angle(degrees: 70)
13-
let offset: CGFloat = 150
14-
let scale: CGFloat = 0.5
15-
1612
Slide(axis: .horizontal)
17-
OnPush {
13+
MirrorPush {
14+
let angle = 70.0
15+
let offset = 150.0
1816
OnInsertion {
19-
Rotate(-angle)
17+
Rotate(.degrees(-angle))
2018
Offset(x: offset)
2119
Opacity()
22-
Scale(scale)
20+
Scale(0.5)
2321
}
2422
OnRemoval {
25-
Rotate(angle)
23+
Rotate(.degrees(angle))
2624
Offset(x: -offset)
2725
}
2826
}
2927
OnPop {
30-
OnInsertion {
31-
Rotate(angle)
32-
Offset(x: -offset)
33-
Opacity()
34-
Scale(scale)
35-
BringToFront()
36-
}
3728
OnRemoval {
38-
Rotate(-angle)
39-
Offset(x: offset)
29+
SendToBack()
4030
}
4131
}
4232
}
4333
}
44-
45-
extension Angle {
46-
static prefix func - (_ rhs: Self) -> Self {
47-
.init(degrees: -rhs.degrees)
48-
}
49-
}

Demo/Demo/Zoom.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import NavigationTransition
2+
import SwiftUI
3+
4+
extension AnyNavigationTransition {
5+
static var zoom: Self {
6+
.init(Zoom())
7+
}
8+
}
9+
10+
struct Zoom: NavigationTransition {
11+
var body: some NavigationTransition {
12+
Slide(axis: .horizontal)
13+
MirrorPush {
14+
Scale(0.5)
15+
}
16+
}
17+
}

Documentation/Custom-Transitions.md

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,39 +47,23 @@ public struct Slide: NavigationTransition {
4747
public var body: some NavigationTransition {
4848
switch axis {
4949
case .horizontal:
50-
OnPush {
50+
MirrorPush {
5151
OnInsertion {
5252
Move(edge: .trailing)
5353
}
5454
OnRemoval {
5555
Move(edge: .leading)
5656
}
5757
}
58-
OnPop {
59-
OnInsertion {
60-
Move(edge: .leading)
61-
}
62-
OnRemoval {
63-
Move(edge: .trailing)
64-
}
65-
}
6658
case .vertical:
67-
OnPush {
59+
MirrorPush {
6860
OnInsertion {
6961
Move(edge: .bottom)
7062
}
7163
OnRemoval {
7264
Move(edge: .top)
7365
}
7466
}
75-
OnPop {
76-
OnInsertion {
77-
Move(edge: .top)
78-
}
79-
OnRemoval {
80-
Move(edge: .bottom)
81-
}
82-
}
8367
}
8468
}
8569
}
@@ -227,9 +211,15 @@ All types conforming to `AtomicTransition` must implement what's known as a "tra
227211

228212
Next up, let's explore two ways of conforming to `NavigationTransition`.
229213

230-
The simplest (and most recommended) way happens by declaring our atomic transitions (if needed), and composing them via `var body: some NavigationTransition { ... }` like we saw [previously with `Slide`](#NavigationTransition).
214+
The simplest (and most recommended) way is by declaring our atomic transitions (if needed), and composing them via `var body: some NavigationTransition { ... }` like we saw [previously with `Slide`](#NavigationTransition).
215+
216+
Check out the [documentation](https://swiftpackageindex.com/davdroman/swiftui-navigation-transitions/0.2.0/documentation/navigationtransitions/navigationtransition) to learn about the different `NavigationTransition` types and how they compose.
217+
218+
The Demo project in the repo is also a great source of learning about different types of custom transitions and the way to implement them.
219+
220+
---
231221

232-
But there's actually an **alternative** option for those who'd like to reach for a more wholistic API. `NavigationTransition` declares this other function that can be implemented instead of `body`:
222+
Finally, let's explore an alternative option for those who'd like to reach for a more wholistic API. `NavigationTransition` declares a `transition` function that can be implemented instead of `body`:
233223

234224
```swift
235225
func transition(from fromView: TransientView, to toView: TransientView, for operation: TransitionOperation, in container: Container)
@@ -241,7 +231,7 @@ Whilst `body` helps composing other transitions, this transition handler helps u
241231
- `Operation` defines whether the operation being performed is a `push` or a `pop`. The concept of insertions or removals is entirely irrelevant to this function, since you can directly modify the property values for the views without needing atomic transitions.
242232
- `Container` is the container view of type `UIView` where `fromView` and `toView` are added during the transition. There's no need to add either view to this container as the library does this for you. Even better, there's no way to even accidentally do it because `TransientView` is not a `UIView` subclass.
243233

244-
This approach is often a simple one to take in case you're working on an app that only requires one custom navigation transition. However, if you're working on an app that features multiple custom transitions, it is recommended that you model your navigation transitions via atomic transitions as described earlier. In the long term, this will be beneficial to your development and iteration speed, by promoting code reusability amongst your team.
234+
This approach is a less cumbersome one to take in case you're working on an app that only requires one custom navigation transition. However, if you're working on an app that features multiple custom transitions, it is recommended that you model your navigation transitions via atomic transitions as described earlier. In the long term, this will be beneficial to your development and iteration speed, by promoting code reusability amongst your team.
245235

246236
### UIKit
247237

README.md

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,34 +96,24 @@ In addition to these, you can create fully [**custom**](Demo/Demo/Swing.swift) t
9696
```swift
9797
struct Swing: NavigationTransition {
9898
var body: some NavigationTransition {
99-
let angle = Angle(degrees: 70)
100-
let offset: CGFloat = 150
101-
let scale: CGFloat = 0.5
102-
10399
Slide(axis: .horizontal)
104-
OnPush {
100+
MirrorPush {
101+
let angle = 70.0
102+
let offset = 150.0
105103
OnInsertion {
106-
Rotate(-angle)
104+
Rotate(.degrees(-angle))
107105
Offset(x: offset)
108106
Opacity()
109-
Scale(scale)
107+
Scale(0.5)
110108
}
111109
OnRemoval {
112-
Rotate(angle)
110+
Rotate(.degrees(angle))
113111
Offset(x: -offset)
114112
}
115113
}
116114
OnPop {
117-
OnInsertion {
118-
Rotate(angle)
119-
Offset(x: -offset)
120-
Opacity()
121-
Scale(scale)
122-
BringToFront()
123-
}
124115
OnRemoval {
125-
Rotate(-angle)
126-
Offset(x: offset)
116+
SendToBack()
127117
}
128118
}
129119
}

Sources/AtomicTransition/Asymmetric.swift

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import class UIKit.UIView
22

3-
/// A composite transition that uses a different transition for insertion versus removal.
3+
/// A composite transition that uses a different transition for push versus pop.
44
public struct Asymmetric<InsertionTransition: AtomicTransition, RemovalTransition: AtomicTransition>: AtomicTransition {
55
private let insertion: InsertionTransition
66
private let removal: RemovalTransition
77

8+
private init(insertion: InsertionTransition, removal: RemovalTransition) {
9+
self.insertion = insertion
10+
self.removal = removal
11+
}
12+
813
public init(
914
@AtomicTransitionBuilder insertion: () -> InsertionTransition,
1015
@AtomicTransitionBuilder removal: () -> RemovalTransition
1116
) {
12-
self.insertion = insertion()
13-
self.removal = removal()
17+
self.init(insertion: insertion(), removal: removal())
1418
}
1519

1620
public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) {
@@ -23,15 +27,25 @@ public struct Asymmetric<InsertionTransition: AtomicTransition, RemovalTransitio
2327
}
2428
}
2529

30+
extension Asymmetric: MirrorableAtomicTransition where InsertionTransition: MirrorableAtomicTransition, RemovalTransition: MirrorableAtomicTransition {
31+
public func mirrored() -> Asymmetric<InsertionTransition.Mirrored, RemovalTransition.Mirrored> {
32+
return .init(insertion: insertion.mirrored(), removal: removal.mirrored())
33+
}
34+
}
35+
2636
extension Asymmetric: Equatable where InsertionTransition: Equatable, RemovalTransition: Equatable {}
2737
extension Asymmetric: Hashable where InsertionTransition: Hashable, RemovalTransition: Hashable {}
2838

2939
/// A transition that executes only on insertion.
3040
public struct OnInsertion<Transition: AtomicTransition>: AtomicTransition {
3141
private let transition: Transition
3242

43+
private init(_ transition: Transition) {
44+
self.transition = transition
45+
}
46+
3347
public init(@AtomicTransitionBuilder transition: () -> Transition) {
34-
self.transition = transition()
48+
self.init(transition())
3549
}
3650

3751
public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) {
@@ -44,15 +58,25 @@ public struct OnInsertion<Transition: AtomicTransition>: AtomicTransition {
4458
}
4559
}
4660

61+
extension OnInsertion: MirrorableAtomicTransition where Transition: MirrorableAtomicTransition {
62+
public func mirrored() -> OnInsertion<Transition.Mirrored> {
63+
.init(transition.mirrored())
64+
}
65+
}
66+
4767
extension OnInsertion: Equatable where Transition: Equatable {}
4868
extension OnInsertion: Hashable where Transition: Hashable {}
4969

5070
/// A transition that executes only on removal.
5171
public struct OnRemoval<Transition: AtomicTransition>: AtomicTransition {
5272
private let transition: Transition
5373

74+
init(_ transition: Transition) {
75+
self.transition = transition
76+
}
77+
5478
public init(@AtomicTransitionBuilder transition: () -> Transition) {
55-
self.transition = transition()
79+
self.init(transition())
5680
}
5781

5882
public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) {
@@ -65,5 +89,11 @@ public struct OnRemoval<Transition: AtomicTransition>: AtomicTransition {
6589
}
6690
}
6791

92+
extension OnRemoval: MirrorableAtomicTransition where Transition: MirrorableAtomicTransition {
93+
public func mirrored() -> OnRemoval<Transition.Mirrored> {
94+
.init(transition.mirrored())
95+
}
96+
}
97+
6898
extension OnRemoval: Equatable where Transition: Equatable {}
6999
extension OnRemoval: Hashable where Transition: Hashable {}

Sources/AtomicTransition/AtomicTransition.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,21 @@ public enum AtomicTransitionOperation {
3030
case insertion
3131
case removal
3232
}
33+
34+
/// Defines an `AtomicTransition` that can be mirrored. It is a specialized building block of `NavigationTransition`.
35+
///
36+
/// A transition that conform to these protocol expose a `Mirrored` associated type expressing the type resulting
37+
/// from mirroring the transition.
38+
public protocol MirrorableAtomicTransition: AtomicTransition {
39+
associatedtype Mirrored: AtomicTransition
40+
41+
/// The mirrored transition.
42+
///
43+
/// > Note: A good indicator of a proper implementation for this function is that it should round-trip
44+
/// > to its original value when called twice:
45+
/// >
46+
/// > ```swift
47+
/// > Offset(x: 10).mirrored().mirrored() == Offset(x: 10)
48+
/// > ```
49+
func mirrored() -> Mirrored
50+
}

Sources/AtomicTransition/Combined.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,11 @@ public struct Combined<TransitionA: AtomicTransition, TransitionB: AtomicTransit
2020
}
2121
}
2222

23+
extension Combined: MirrorableAtomicTransition where TransitionA: MirrorableAtomicTransition, TransitionB: MirrorableAtomicTransition {
24+
public func mirrored() -> Combined<TransitionA.Mirrored, TransitionB.Mirrored> {
25+
.init(transitionA.mirrored(), transitionB.mirrored())
26+
}
27+
}
28+
2329
extension Combined: Equatable where TransitionA: Equatable, TransitionB: Equatable {}
2430
extension Combined: Hashable where TransitionA: Hashable, TransitionB: Hashable {}

Sources/AtomicTransition/Group.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,24 @@ import class UIKit.UIView
44
public struct Group<Transitions: AtomicTransition>: AtomicTransition {
55
private let transitions: Transitions
66

7+
private init(_ transitions: Transitions) {
8+
self.transitions = transitions
9+
}
10+
711
public init(@AtomicTransitionBuilder _ transitions: () -> Transitions) {
8-
self.transitions = transitions()
12+
self.init(transitions())
913
}
1014

1115
public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) {
1216
transitions.transition(view, for: operation, in: container)
1317
}
1418
}
1519

20+
extension Group: MirrorableAtomicTransition where Transitions: MirrorableAtomicTransition {
21+
public func mirrored() -> Group<Transitions.Mirrored> {
22+
.init(transitions.mirrored())
23+
}
24+
}
25+
1626
extension Group: Equatable where Transitions: Equatable {}
1727
extension Group: Hashable where Transitions: Hashable {}

0 commit comments

Comments
 (0)