Skip to content

Commit 37483a4

Browse files
authored
Rotate3D Transition (#37)
Closes #8. This one also includes a nice cleanup of transient view props and unifies transforms by prioritizing 2D if possible and using 3D only if necessary.
1 parent b26f794 commit 37483a4

26 files changed

+306
-348
lines changed

Demo/Demo.xcodeproj/project.pbxproj

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
D5535845290F52F7009E5D72 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5535844290F52F7009E5D72 /* SettingsView.swift */; };
2020
D5535847290F5E6F009E5D72 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5535846290F5E6F009E5D72 /* AppState.swift */; };
2121
D5755A79291ADC00007F2201 /* Zoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5755A78291ADC00007F2201 /* Zoom.swift */; };
22+
D58D803F292176D200D9FEAE /* Flip.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58D803E292176D200D9FEAE /* Flip.swift */; };
2223
D5AAF4052911C59E009743D3 /* PageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF4042911C59E009743D3 /* PageView.swift */; };
2324
D5AAF4072911C621009743D3 /* Pages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF4062911C621009743D3 /* Pages.swift */; };
2425
/* End PBXBuildFile section */
@@ -38,6 +39,7 @@
3839
D5535846290F5E6F009E5D72 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
3940
D571826B291C9426003672F5 /* Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Demo.entitlements; sourceTree = "<group>"; };
4041
D5755A78291ADC00007F2201 /* Zoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zoom.swift; sourceTree = "<group>"; };
42+
D58D803E292176D200D9FEAE /* Flip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Flip.swift; sourceTree = "<group>"; };
4143
D5AAF4042911C59E009743D3 /* PageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageView.swift; sourceTree = "<group>"; };
4244
D5AAF4062911C621009743D3 /* Pages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pages.swift; sourceTree = "<group>"; };
4345
/* End PBXFileReference section */
@@ -85,8 +87,7 @@
8587
D5AAF4062911C621009743D3 /* Pages.swift */,
8688
D5AAF4042911C59E009743D3 /* PageView.swift */,
8789
D5535844290F52F7009E5D72 /* SettingsView.swift */,
88-
D553582C290E9718009E5D72 /* Swing.swift */,
89-
D5755A78291ADC00007F2201 /* Zoom.swift */,
90+
D58D8040292176EE00D9FEAE /* Custom Transitions */,
9091
D5535822290E9692009E5D72 /* Assets.xcassets */,
9192
D5535824290E9692009E5D72 /* Preview Content */,
9293
);
@@ -108,6 +109,16 @@
108109
name = Frameworks;
109110
sourceTree = "<group>";
110111
};
112+
D58D8040292176EE00D9FEAE /* Custom Transitions */ = {
113+
isa = PBXGroup;
114+
children = (
115+
D58D803E292176D200D9FEAE /* Flip.swift */,
116+
D553582C290E9718009E5D72 /* Swing.swift */,
117+
D5755A78291ADC00007F2201 /* Zoom.swift */,
118+
);
119+
path = "Custom Transitions";
120+
sourceTree = "<group>";
121+
};
111122
/* End PBXGroup section */
112123

113124
/* Begin PBXNativeTarget section */
@@ -186,6 +197,7 @@
186197
D5AAF4072911C621009743D3 /* Pages.swift in Sources */,
187198
D5535845290F52F7009E5D72 /* SettingsView.swift in Sources */,
188199
D5755A79291ADC00007F2201 /* Zoom.swift in Sources */,
200+
D58D803F292176D200D9FEAE /* Flip.swift in Sources */,
189201
D5AAF4052911C59E009743D3 /* PageView.swift in Sources */,
190202
D5535839290E9718009E5D72 /* AppDelegate.swift in Sources */,
191203
D5535847290F5E6F009E5D72 /* AppState.swift in Sources */,

Demo/Demo/AppState.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ final class AppState: ObservableObject {
66
case `default`
77
case slide
88
case crossFade
9+
case flip
10+
case flipVertically
911
case slideAndFadeIn
1012
case slideAndFadeOut
1113
case moveVertically
@@ -21,6 +23,10 @@ final class AppState: ObservableObject {
2123
return "Slide"
2224
case .crossFade:
2325
return "Fade"
26+
case .flip:
27+
return "Flip"
28+
case .flipVertically:
29+
return "Flip Vertically"
2430
case .slideAndFadeIn:
2531
return "Slide + Fade In"
2632
case .slideAndFadeOut:
@@ -44,6 +50,10 @@ final class AppState: ObservableObject {
4450
return .slide
4551
case .crossFade:
4652
return .fade(.cross)
53+
case .flip:
54+
return .flip
55+
case .flipVertically:
56+
return .flip(axis: .vertical)
4757
case .slideAndFadeIn:
4858
return .slide.combined(with: .fade(.in))
4959
case .slideAndFadeOut:
@@ -53,7 +63,7 @@ final class AppState: ObservableObject {
5363
case .swing:
5464
return .swing
5565
case .zoom:
56-
return .zoom.combined(with: .fade(.in))
66+
return .zoom
5767
case .zoomAndSlide:
5868
return .zoom.combined(with: .slide)
5969
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import NavigationTransition
2+
import SwiftUI
3+
4+
extension AnyNavigationTransition {
5+
static func flip(axis: Axis) -> Self {
6+
.init(Flip(axis: axis))
7+
}
8+
9+
static var flip: Self {
10+
.flip(axis: .horizontal)
11+
}
12+
}
13+
14+
struct Flip: NavigationTransition {
15+
var axis: Axis
16+
17+
var body: some NavigationTransition {
18+
MirrorPush {
19+
Rotate3D(.degrees(180), axis: axis == .horizontal ? (x: 1, y: 0, z: 0) : (x: 0, y: 1, z: 0))
20+
}
21+
}
22+
}
File renamed without changes.

Demo/Demo/Zoom.swift renamed to Demo/Demo/Custom Transitions/Zoom.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ struct Zoom: NavigationTransition {
1111
var body: some NavigationTransition {
1212
MirrorPush {
1313
Scale(0.5)
14+
OnInsertion {
15+
ZPosition(1)
16+
Opacity()
17+
}
1418
}
1519
}
1620
}

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdavdroman%2Fswiftui-navigation-transitions%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/davdroman/swiftui-navigation-transitions)
66

77
<p align="center">
8-
<img width="320" src="https://user-images.githubusercontent.com/2538074/199754334-7f2f801d-1d9e-4cc4-a7a0-bb22c9835007.gif">
8+
<img width="320" src="https://user-images.githubusercontent.com/2538074/201549712-4234ca45-bdeb-42c4-9ee9-8d44b346ecdd.gif">
9+
<img width="320" src="https://user-images.githubusercontent.com/2538074/201549897-147e90a0-3773-42ab-94bc-1065fbb7a66b.gif">
10+
<img width="320" src="https://user-images.githubusercontent.com/2538074/201549995-62b86d4a-aa8b-4a6e-9bb4-5ed70cd47d84.gif">
11+
<img width="320" src="https://user-images.githubusercontent.com/2538074/201550282-64ce0f8e-8f99-4fe2-baf8-583e35c0518a.gif">
912
</p>
1013

1114
**NavigationTransitions** is a library that integrates seamlessly with SwiftUI's **Navigation** views, allowing complete customization over **push and pop transitions**!
@@ -17,7 +20,7 @@ The library is fully compatible with:
1720

1821
## Overview
1922

20-
Instead of reinventing entire navigation components in order to customize its transitions, `NavigationTransitions` ships with a simple modifier that can be applied directly to SwiftUI's very own first-party navigation component.
23+
Instead of reinventing the entire navigation stack just to control its transitions, `NavigationTransitions` ships with a **simple modifier** that can be applied directly to SwiftUI's very own **first-party navigation** components.
2124

2225
### The Basics
2326

Sources/Animator/AnimatorTransientView.swift

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,9 @@ public class AnimatorTransientView {
4040
}
4141

4242
@_spi(package)public init(_ uiView: UIView) {
43-
let properties = Properties(
44-
alpha: uiView.alpha,
45-
transform: uiView.transform,
46-
layer: .init(
47-
zPosition: uiView.layer.zPosition
48-
)
49-
)
50-
self.initial = properties
51-
self.animation = properties
52-
self.completion = properties
43+
self.initial = Properties(of: uiView)
44+
self.animation = Properties(of: uiView)
45+
self.completion = Properties(of: uiView)
5346

5447
self.uiView = uiView
5548
}

Sources/Animator/AnimatorTransientViewLayerProperties.swift

Lines changed: 0 additions & 12 deletions
This file was deleted.

Sources/Animator/AnimatorTransientViewProperties.swift

Lines changed: 14 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2,103 +2,31 @@ import UIKit
22

33
/// Defines the allowed mutable properties in a transient view throughout each stage of the transition.
44
public struct AnimatorTransientViewProperties: Equatable {
5-
public typealias Layer = AnimatorTransientViewLayerProperties
6-
75
/// A proxy for `UIView.alpha`.
86
@OptionalWithDefault
97
public var alpha: CGFloat
108

11-
/// A proxy for `UIView.transform`.
9+
/// A proxy for `UIView.transform` or `UIView.transform3D`.
1210
@OptionalWithDefault
13-
public var transform: CGAffineTransform
11+
public var transform: Transform
1412

15-
/// A proxy for `UIView.layer`.
13+
/// A proxy for `UIView.layer.zPosition`.
1614
@OptionalWithDefault
17-
public var layer: Layer
18-
19-
func assignToUIView(_ uiView: UIView) {
20-
$alpha.assignTo(uiView, \.alpha)
21-
$transform.assignTo(uiView, \.transform)
22-
$layer?.assignToUIView(uiView)
23-
}
15+
public var zPosition: CGFloat
2416
}
2517

2618
extension AnimatorTransientViewProperties {
27-
/// Convenience property for `CGAffineTransform` translation component.
28-
public var translation: CGVector {
29-
get { transform.translation }
30-
set { transform.translation = newValue }
31-
}
32-
33-
/// Convenience property for `CGAffineTransform` scale component.
34-
public var scale: CGSize {
35-
get { transform.scale }
36-
set { transform.scale = newValue }
37-
}
38-
39-
/// Convenience property for `CGAffineTransform` rotation component.
40-
public var rotation: CGFloat {
41-
get { transform.rotation }
42-
set { transform.rotation = newValue }
43-
}
44-
}
45-
46-
extension CGAffineTransform {
47-
var translation: CGVector {
48-
get { components.translation }
49-
set { components.translation = newValue }
50-
}
51-
52-
var scale: CGSize {
53-
get { components.scale }
54-
set { components.scale = newValue }
19+
init(of uiView: UIView) {
20+
self.init(
21+
alpha: uiView.alpha,
22+
transform: .init(uiView.transform3D),
23+
zPosition: uiView.layer.zPosition
24+
)
5525
}
5626

57-
var rotation: CGFloat {
58-
get { components.rotation }
59-
set { components.rotation = newValue }
60-
}
61-
62-
private typealias Components = _CGAffineTransformComponents
63-
64-
private var components: Components {
65-
get {
66-
Components(
67-
scale: CGSize(
68-
width: sqrt(pow(a, 2) + pow(c, 2)),
69-
height: sqrt(pow(b, 2) + pow(d, 2))
70-
),
71-
rotation: atan2(b, a),
72-
translation: CGVector(dx: tx, dy: ty)
73-
)
74-
}
75-
set {
76-
guard components != newValue else { return }
77-
self = CGAffineTransform(newValue)
78-
}
79-
}
80-
81-
private init(_ components: Components) {
82-
self = .identity
83-
.translatedBy(x: components.translation.dx, y: components.translation.dy)
84-
.scaledBy(x: components.scale.width, y: components.scale.height)
85-
.rotated(by: components.rotation)
86-
}
87-
}
88-
89-
public struct _CGAffineTransformComponents: Equatable {
90-
/// Scaling in X and Y dimensions.
91-
public var scale: CGSize
92-
93-
/// Rotation angle in radians.
94-
public var rotation: Double
95-
96-
/// Displacement from the origin (ty, ty).
97-
public var translation: CGVector
98-
99-
public init(scale: CGSize, rotation: Double, translation: CGVector) {
100-
self.scale = scale
101-
self.rotation = rotation
102-
self.translation = translation
27+
func assignToUIView(_ uiView: UIView) {
28+
$alpha.assignTo(uiView, \.alpha)
29+
$transform?.assignToUIView(uiView)
30+
$zPosition.assignTo(uiView, \.layer.zPosition)
10331
}
10432
}

Sources/Animator/Transform.swift

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import UIKit
2+
3+
@dynamicMemberLookup
4+
public struct Transform: Equatable {
5+
private var transform: CATransform3D
6+
7+
public subscript<T>(dynamicMember keyPath: WritableKeyPath<CATransform3D, T>) -> T {
8+
get { transform[keyPath: keyPath] }
9+
set { transform[keyPath: keyPath] = newValue }
10+
}
11+
12+
init(_ transform: CATransform3D) {
13+
self.transform = transform
14+
}
15+
16+
func assignToUIView(_ uiView: UIView) {
17+
if let transform = transform.affineTransform {
18+
uiView.transform = transform
19+
} else {
20+
uiView.transform3D = transform
21+
}
22+
}
23+
}
24+
25+
extension CATransform3D {
26+
var affineTransform: CGAffineTransform? {
27+
guard CATransform3DIsAffine(self) else {
28+
return nil
29+
}
30+
return CATransform3DGetAffineTransform(self)
31+
}
32+
}
33+
34+
@_spi(package)
35+
extension CATransform3D: Equatable {
36+
@inlinable
37+
public static func == (lhs: Self, rhs: Self) -> Bool {
38+
CATransform3DEqualToTransform(lhs, rhs)
39+
}
40+
}
41+
42+
extension Transform {
43+
public static var identity: Self {
44+
.init(.identity)
45+
}
46+
47+
public mutating func translate(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) {
48+
transform = transform.translated(x: x, y: y, z: z)
49+
}
50+
51+
public mutating func scale(x: CGFloat = 1, y: CGFloat = 1, z: CGFloat = 1) {
52+
transform = transform.scaled(x: x, y: y, z: z)
53+
}
54+
55+
public mutating func scale(_ s: CGFloat) {
56+
transform = transform.scaled(x: s, y: s, z: s)
57+
}
58+
59+
public mutating func rotate(by angle: CGFloat, x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) {
60+
transform = transform.rotated(by: angle, x: x, y: y, z: z)
61+
}
62+
63+
public func concatenated(with other: Self) -> Self {
64+
.init(transform.concatenated(with: other.transform))
65+
}
66+
}
67+
68+
extension CATransform3D {
69+
@inlinable
70+
static var identity: Self {
71+
CATransform3DIdentity
72+
}
73+
74+
@inlinable
75+
func translated(x: CGFloat, y: CGFloat, z: CGFloat) -> CATransform3D {
76+
CATransform3DTranslate(self, x, y, z)
77+
}
78+
79+
@inlinable
80+
func scaled(x: CGFloat, y: CGFloat, z: CGFloat) -> CATransform3D {
81+
CATransform3DScale(self, x, y, z)
82+
}
83+
84+
@inlinable
85+
func scaled(_ s: CGFloat) -> CATransform3D {
86+
CATransform3DScale(self, s, s, s)
87+
}
88+
89+
@inlinable
90+
func rotated(by angle: CGFloat, x: CGFloat, y: CGFloat, z: CGFloat) -> CATransform3D {
91+
CATransform3DRotate(self, angle, x, y, z)
92+
}
93+
94+
@inlinable
95+
func concatenated(with other: CATransform3D) -> CATransform3D {
96+
CATransform3DConcat(self, other)
97+
}
98+
}

0 commit comments

Comments
 (0)