From fea35020ad98c8415851b03d57fcd62ba2cb6e2a Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 16:58:26 +0000 Subject: [PATCH 01/16] wip --- .../AtomicTransition/AtomicTransition.swift | 6 ++++ Sources/NavigationTransition/Mirror.swift | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 Sources/NavigationTransition/Mirror.swift diff --git a/Sources/AtomicTransition/AtomicTransition.swift b/Sources/AtomicTransition/AtomicTransition.swift index f2a62e03..33eda4e0 100644 --- a/Sources/AtomicTransition/AtomicTransition.swift +++ b/Sources/AtomicTransition/AtomicTransition.swift @@ -30,3 +30,9 @@ public enum AtomicTransitionOperation { case insertion case removal } + +public protocol MirrorableAtomicTransition: AtomicTransition { + associatedtype Mirrored: AtomicTransition + + func mirrored() -> Mirrored +} diff --git a/Sources/NavigationTransition/Mirror.swift b/Sources/NavigationTransition/Mirror.swift new file mode 100644 index 00000000..661c1e54 --- /dev/null +++ b/Sources/NavigationTransition/Mirror.swift @@ -0,0 +1,35 @@ +import AtomicTransition + +public struct MirrorPush: NavigationTransition { + private let transition: Transition + + init(@AtomicTransitionBuilder transition: () -> Transition) { + self.transition = transition() + } + + public var body: some NavigationTransition { + OnPush { + transition + } + OnPop { + transition.mirrored() + } + } +} + +public struct MirrorPop: NavigationTransition { + private let transition: Transition + + init(@AtomicTransitionBuilder transition: () -> Transition) { + self.transition = transition() + } + + public var body: some NavigationTransition { + OnPush { + transition.mirrored() + } + OnPop { + transition + } + } +} From efbda4b9887504d2f7daf18abdbdb0b1113e152f Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 17:13:03 +0000 Subject: [PATCH 02/16] wip --- Sources/AtomicTransition/Asymmetric.swift | 38 ++++++++++++++++++++--- Sources/AtomicTransition/Combined.swift | 6 ++++ Sources/AtomicTransition/Group.swift | 12 ++++++- Sources/AtomicTransition/Identity.swift | 7 ++++- 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/Sources/AtomicTransition/Asymmetric.swift b/Sources/AtomicTransition/Asymmetric.swift index 97ac8116..5d1a6d7a 100644 --- a/Sources/AtomicTransition/Asymmetric.swift +++ b/Sources/AtomicTransition/Asymmetric.swift @@ -5,12 +5,16 @@ public struct Asymmetric InsertionTransition, @AtomicTransitionBuilder removal: () -> RemovalTransition ) { - self.insertion = insertion() - self.removal = removal() + self.init(insertion: insertion(), removal: removal()) } public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { @@ -23,6 +27,12 @@ public struct Asymmetric Asymmetric { + return .init(insertion: insertion.mirrored(), removal: removal.mirrored()) + } +} + extension Asymmetric: Equatable where InsertionTransition: Equatable, RemovalTransition: Equatable {} extension Asymmetric: Hashable where InsertionTransition: Hashable, RemovalTransition: Hashable {} @@ -30,8 +40,12 @@ extension Asymmetric: Hashable where InsertionTransition: Hashable, RemovalTrans public struct OnInsertion: AtomicTransition { private let transition: Transition + init(_ transition: Transition) { + self.transition = transition + } + public init(@AtomicTransitionBuilder transition: () -> Transition) { - self.transition = transition() + self.init(transition()) } public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { @@ -44,6 +58,12 @@ public struct OnInsertion: AtomicTransition { } } +extension OnInsertion: MirrorableAtomicTransition where Transition: MirrorableAtomicTransition { + public func mirrored() -> OnInsertion { + .init(transition.mirrored()) + } +} + extension OnInsertion: Equatable where Transition: Equatable {} extension OnInsertion: Hashable where Transition: Hashable {} @@ -51,8 +71,12 @@ extension OnInsertion: Hashable where Transition: Hashable {} public struct OnRemoval: AtomicTransition { private let transition: Transition + init(_ transition: Transition) { + self.transition = transition + } + public init(@AtomicTransitionBuilder transition: () -> Transition) { - self.transition = transition() + self.init(transition()) } public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { @@ -65,5 +89,11 @@ public struct OnRemoval: AtomicTransition { } } +extension OnRemoval: MirrorableAtomicTransition where Transition: MirrorableAtomicTransition { + public func mirrored() -> OnRemoval { + .init(transition.mirrored()) + } +} + extension OnRemoval: Equatable where Transition: Equatable {} extension OnRemoval: Hashable where Transition: Hashable {} diff --git a/Sources/AtomicTransition/Combined.swift b/Sources/AtomicTransition/Combined.swift index e13e1929..20dd4259 100644 --- a/Sources/AtomicTransition/Combined.swift +++ b/Sources/AtomicTransition/Combined.swift @@ -20,5 +20,11 @@ public struct Combined Combined { + .init(transitionA.mirrored(), transitionB.mirrored()) + } +} + extension Combined: Equatable where TransitionA: Equatable, TransitionB: Equatable {} extension Combined: Hashable where TransitionA: Hashable, TransitionB: Hashable {} diff --git a/Sources/AtomicTransition/Group.swift b/Sources/AtomicTransition/Group.swift index a4b2e505..94d69a02 100644 --- a/Sources/AtomicTransition/Group.swift +++ b/Sources/AtomicTransition/Group.swift @@ -4,8 +4,12 @@ import class UIKit.UIView public struct Group: AtomicTransition { private let transitions: Transitions + init(_ transitions: Transitions) { + self.transitions = transitions + } + public init(@AtomicTransitionBuilder _ transitions: () -> Transitions) { - self.transitions = transitions() + self.init(transitions()) } public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { @@ -13,5 +17,11 @@ public struct Group: AtomicTransition { } } +extension Group: MirrorableAtomicTransition where Transitions: MirrorableAtomicTransition { + public func mirrored() -> Group { + .init(transitions.mirrored()) + } +} + extension Group: Equatable where Transitions: Equatable {} extension Group: Hashable where Transitions: Hashable {} diff --git a/Sources/AtomicTransition/Identity.swift b/Sources/AtomicTransition/Identity.swift index 84ce9926..35134607 100644 --- a/Sources/AtomicTransition/Identity.swift +++ b/Sources/AtomicTransition/Identity.swift @@ -1,12 +1,17 @@ import class UIKit.UIView /// A transition that returns the input view, unmodified, as the output view. -public struct Identity: AtomicTransition { +public struct Identity: AtomicTransition, MirrorableAtomicTransition { public init() {} public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { // NO-OP } + + @_transparent + public func mirrored() -> Identity { + self + } } extension Identity: Hashable {} From ed9be29086c66df5c7b29beeef9671196a66119a Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 17:25:08 +0000 Subject: [PATCH 03/16] wip --- Sources/AtomicTransition/Identity.swift | 2 +- Sources/AtomicTransition/Move.swift | 15 ++++++++++++++- Sources/AtomicTransition/Offset.swift | 6 +++++- Sources/AtomicTransition/Opacity.swift | 6 +++++- Sources/AtomicTransition/Rotate.swift | 6 +++++- Sources/AtomicTransition/Scale.swift | 6 +++++- Sources/AtomicTransition/ZPosition.swift | 12 ++++++++++-- Sources/NavigationTransition/Fade.swift | 20 +++----------------- Sources/NavigationTransition/Slide.swift | 20 ++------------------ 9 files changed, 50 insertions(+), 43 deletions(-) diff --git a/Sources/AtomicTransition/Identity.swift b/Sources/AtomicTransition/Identity.swift index 35134607..3845bfc7 100644 --- a/Sources/AtomicTransition/Identity.swift +++ b/Sources/AtomicTransition/Identity.swift @@ -9,7 +9,7 @@ public struct Identity: AtomicTransition, MirrorableAtomicTransition { } @_transparent - public func mirrored() -> Identity { + public func mirrored() -> Self { self } } diff --git a/Sources/AtomicTransition/Move.swift b/Sources/AtomicTransition/Move.swift index 8c22eb49..7b948828 100644 --- a/Sources/AtomicTransition/Move.swift +++ b/Sources/AtomicTransition/Move.swift @@ -1,7 +1,7 @@ import SwiftUI /// A transition entering from `edge` on insertion, and exiting towards `edge` on removal. -public struct Move: AtomicTransition { +public struct Move: MirrorableAtomicTransition { private let edge: Edge public init(edge: Edge) { @@ -43,6 +43,19 @@ public struct Move: AtomicTransition { view.completion.translation.dy = 0 } } + + public func mirrored() -> Move { + switch edge { + case .top: + return .init(edge: .bottom) + case .leading: + return .init(edge: .trailing) + case .bottom: + return .init(edge: .top) + case .trailing: + return .init(edge: .leading) + } + } } extension Move: Hashable {} diff --git a/Sources/AtomicTransition/Offset.swift b/Sources/AtomicTransition/Offset.swift index 49c554d6..bf0165ac 100644 --- a/Sources/AtomicTransition/Offset.swift +++ b/Sources/AtomicTransition/Offset.swift @@ -1,7 +1,7 @@ import UIKit /// A transition that translates the view from offset to zero on insertion, and from zero to offset on removal. -public struct Offset: AtomicTransition { +public struct Offset: MirrorableAtomicTransition { private let x: CGFloat private let y: CGFloat @@ -34,6 +34,10 @@ public struct Offset: AtomicTransition { view.completion.transform = .identity } } + + public func mirrored() -> Offset { + .init(x: -x, y: -y) + } } extension Offset: Hashable {} diff --git a/Sources/AtomicTransition/Opacity.swift b/Sources/AtomicTransition/Opacity.swift index 9bb4deef..8c4ed4fd 100644 --- a/Sources/AtomicTransition/Opacity.swift +++ b/Sources/AtomicTransition/Opacity.swift @@ -1,7 +1,7 @@ import class UIKit.UIView /// A transition from transparent to opaque on insertion, and from opaque to transparent on removal. -public struct Opacity: AtomicTransition { +public struct Opacity: MirrorableAtomicTransition { public init() {} public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { @@ -14,6 +14,10 @@ public struct Opacity: AtomicTransition { view.completion.alpha = 1 } } + + public func mirrored() -> Self { + self + } } extension Opacity: Hashable {} diff --git a/Sources/AtomicTransition/Rotate.swift b/Sources/AtomicTransition/Rotate.swift index 9ab908a2..ede148db 100644 --- a/Sources/AtomicTransition/Rotate.swift +++ b/Sources/AtomicTransition/Rotate.swift @@ -1,7 +1,7 @@ import SwiftUI /// A transition that rotates the view from `angle` to zero on insertion, and from zero to `angle` on removal. -public struct Rotate: AtomicTransition { +public struct Rotate: MirrorableAtomicTransition { private let angle: Angle public init(_ angle: Angle) { @@ -18,6 +18,10 @@ public struct Rotate: AtomicTransition { view.completion.transform = .identity } } + + public func mirrored() -> Rotate { + .init(.radians(-angle.radians)) + } } extension Rotate: Hashable {} diff --git a/Sources/AtomicTransition/Scale.swift b/Sources/AtomicTransition/Scale.swift index ca8db71d..39edc677 100644 --- a/Sources/AtomicTransition/Scale.swift +++ b/Sources/AtomicTransition/Scale.swift @@ -1,7 +1,7 @@ import UIKit /// A transition that scales the view from `scale` to `1` on insertion, and from `1` to `scale` on removal. -public struct Scale: AtomicTransition { +public struct Scale: MirrorableAtomicTransition { private let scale: CGFloat /// Returns a transition that scales the view from `scale` to 1.0 on insertion, and from 1.0 to `scale` on removal. @@ -27,6 +27,10 @@ public struct Scale: AtomicTransition { view.completion.transform = .identity } } + + public func mirrored() -> Self { + self + } } extension Scale: Hashable {} diff --git a/Sources/AtomicTransition/ZPosition.swift b/Sources/AtomicTransition/ZPosition.swift index 640f00a5..91d0e1d0 100644 --- a/Sources/AtomicTransition/ZPosition.swift +++ b/Sources/AtomicTransition/ZPosition.swift @@ -2,23 +2,31 @@ import class UIKit.UIView /// A transition that brings the view to the front, regardless of insertion or removal. -public struct BringToFront: AtomicTransition { +public struct BringToFront: MirrorableAtomicTransition { public init() {} public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { container.bringSubviewToFront(view.uiView) } + + public func mirrored() -> SendToBack { + SendToBack() + } } extension BringToFront: Hashable {} /// A transition that sends the view to the back, regardless of insertion or removal. -public struct SendToBack: AtomicTransition { +public struct SendToBack: MirrorableAtomicTransition { public init() {} public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { container.sendSubviewToBack(view.uiView) } + + public func mirrored() -> some AtomicTransition { + BringToFront() + } } extension SendToBack: Hashable {} diff --git a/Sources/NavigationTransition/Fade.swift b/Sources/NavigationTransition/Fade.swift index d2a1053c..5ba1d5dc 100644 --- a/Sources/NavigationTransition/Fade.swift +++ b/Sources/NavigationTransition/Fade.swift @@ -24,34 +24,20 @@ public struct Fade: NavigationTransition { public var body: some NavigationTransition { switch style { case .in: - OnPush { + MirrorPush { OnInsertion { Opacity() } } - OnPop { - OnRemoval { - Opacity() - } - } case .out: - OnPush { + MirrorPush { OnRemoval { BringToFront() Opacity() } } - OnPop { - OnInsertion { - BringToFront() - Opacity() - } - } case .cross: - OnPush { - Opacity() - } - OnPop { + MirrorPush { Opacity() } } diff --git a/Sources/NavigationTransition/Slide.swift b/Sources/NavigationTransition/Slide.swift index cdc109b1..f73951f0 100644 --- a/Sources/NavigationTransition/Slide.swift +++ b/Sources/NavigationTransition/Slide.swift @@ -41,7 +41,7 @@ public struct Slide: NavigationTransition { public var body: some NavigationTransition { switch axis { case .horizontal: - OnPush { + MirrorPush { OnInsertion { Move(edge: .trailing) } @@ -49,16 +49,8 @@ public struct Slide: NavigationTransition { Move(edge: .leading) } } - OnPop { - OnInsertion { - Move(edge: .leading) - } - OnRemoval { - Move(edge: .trailing) - } - } case .vertical: - OnPush { + MirrorPush { OnInsertion { Move(edge: .bottom) } @@ -66,14 +58,6 @@ public struct Slide: NavigationTransition { Move(edge: .top) } } - OnPop { - OnInsertion { - Move(edge: .top) - } - OnRemoval { - Move(edge: .bottom) - } - } } } } From be958600c554b477a98eb51bf34826509ecca4e9 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 17:50:55 +0000 Subject: [PATCH 04/16] wip --- Demo/Demo/Swing.swift | 15 +++------------ Sources/NavigationTransition/Fade.swift | 17 +++++++++++++---- Sources/NavigationTransition/Mirror.swift | 7 +++++-- Sources/NavigationTransition/Slide.swift | 4 ++-- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Demo/Demo/Swing.swift b/Demo/Demo/Swing.swift index 505dab8e..082ee8fb 100644 --- a/Demo/Demo/Swing.swift +++ b/Demo/Demo/Swing.swift @@ -11,15 +11,14 @@ struct Swing: NavigationTransition { var body: some NavigationTransition { let angle = Angle(degrees: 70) let offset: CGFloat = 150 - let scale: CGFloat = 0.5 Slide(axis: .horizontal) - OnPush { + Mirror { OnInsertion { Rotate(-angle) Offset(x: offset) Opacity() - Scale(scale) + Scale(0.5) } OnRemoval { Rotate(angle) @@ -27,16 +26,8 @@ struct Swing: NavigationTransition { } } OnPop { - OnInsertion { - Rotate(angle) - Offset(x: -offset) - Opacity() - Scale(scale) - BringToFront() - } OnRemoval { - Rotate(-angle) - Offset(x: offset) + SendToBack() } } } diff --git a/Sources/NavigationTransition/Fade.swift b/Sources/NavigationTransition/Fade.swift index 5ba1d5dc..835b4803 100644 --- a/Sources/NavigationTransition/Fade.swift +++ b/Sources/NavigationTransition/Fade.swift @@ -24,20 +24,29 @@ public struct Fade: NavigationTransition { public var body: some NavigationTransition { switch style { case .in: - MirrorPush { + Mirror { OnInsertion { Opacity() } } + OnPop { + OnInsertion { + BringToFront() + } + } case .out: - MirrorPush { + Mirror { OnRemoval { - BringToFront() Opacity() } } + OnPush { + OnRemoval { + BringToFront() + } + } case .cross: - MirrorPush { + Mirror { Opacity() } } diff --git a/Sources/NavigationTransition/Mirror.swift b/Sources/NavigationTransition/Mirror.swift index 661c1e54..f07b039a 100644 --- a/Sources/NavigationTransition/Mirror.swift +++ b/Sources/NavigationTransition/Mirror.swift @@ -1,9 +1,12 @@ import AtomicTransition +/// Typealias for `MirrorPush`. +public typealias Mirror = MirrorPush + public struct MirrorPush: NavigationTransition { private let transition: Transition - init(@AtomicTransitionBuilder transition: () -> Transition) { + public init(@AtomicTransitionBuilder transition: () -> Transition) { self.transition = transition() } @@ -20,7 +23,7 @@ public struct MirrorPush: NavigationTran public struct MirrorPop: NavigationTransition { private let transition: Transition - init(@AtomicTransitionBuilder transition: () -> Transition) { + public init(@AtomicTransitionBuilder transition: () -> Transition) { self.transition = transition() } diff --git a/Sources/NavigationTransition/Slide.swift b/Sources/NavigationTransition/Slide.swift index f73951f0..f505de7e 100644 --- a/Sources/NavigationTransition/Slide.swift +++ b/Sources/NavigationTransition/Slide.swift @@ -41,7 +41,7 @@ public struct Slide: NavigationTransition { public var body: some NavigationTransition { switch axis { case .horizontal: - MirrorPush { + Mirror { OnInsertion { Move(edge: .trailing) } @@ -50,7 +50,7 @@ public struct Slide: NavigationTransition { } } case .vertical: - MirrorPush { + Mirror { OnInsertion { Move(edge: .bottom) } From a94cbeae90d7e41cfa9fb39c2e4d8e77484172db Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 17:51:59 +0000 Subject: [PATCH 05/16] wip --- Sources/AtomicTransition/ZPosition.swift | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Sources/AtomicTransition/ZPosition.swift b/Sources/AtomicTransition/ZPosition.swift index 91d0e1d0..640f00a5 100644 --- a/Sources/AtomicTransition/ZPosition.swift +++ b/Sources/AtomicTransition/ZPosition.swift @@ -2,31 +2,23 @@ import class UIKit.UIView /// A transition that brings the view to the front, regardless of insertion or removal. -public struct BringToFront: MirrorableAtomicTransition { +public struct BringToFront: AtomicTransition { public init() {} public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { container.bringSubviewToFront(view.uiView) } - - public func mirrored() -> SendToBack { - SendToBack() - } } extension BringToFront: Hashable {} /// A transition that sends the view to the back, regardless of insertion or removal. -public struct SendToBack: MirrorableAtomicTransition { +public struct SendToBack: AtomicTransition { public init() {} public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { container.sendSubviewToBack(view.uiView) } - - public func mirrored() -> some AtomicTransition { - BringToFront() - } } extension SendToBack: Hashable {} From f00a5f1c3b4ef22afcf8ea8a5d33a7462693126b Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 19:01:11 +0000 Subject: [PATCH 06/16] wip --- Demo/Demo.xcodeproj/project.pbxproj | 4 ++ Demo/Demo/AppState.swift | 5 ++ Demo/Demo/Swing.swift | 2 +- Demo/Demo/Zoom.swift | 17 +++++++ Sources/AtomicTransition/Group.swift | 2 +- Sources/AtomicTransition/Mirror.swift | 61 +++++++++++++++++++++++ Sources/NavigationTransition/Fade.swift | 6 +-- Sources/NavigationTransition/Mirror.swift | 3 -- Sources/NavigationTransition/Slide.swift | 4 +- 9 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 Demo/Demo/Zoom.swift create mode 100644 Sources/AtomicTransition/Mirror.swift diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index a2e533bc..48c2ce54 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ D5535843290F4BEA009E5D72 /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5535842290F4BEA009E5D72 /* AppView.swift */; }; D5535845290F52F7009E5D72 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5535844290F52F7009E5D72 /* SettingsView.swift */; }; D5535847290F5E6F009E5D72 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5535846290F5E6F009E5D72 /* AppState.swift */; }; + D5755A79291ADC00007F2201 /* Zoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5755A78291ADC00007F2201 /* Zoom.swift */; }; D5AAF4052911C59E009743D3 /* PageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF4042911C59E009743D3 /* PageView.swift */; }; D5AAF4072911C621009743D3 /* Pages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF4062911C621009743D3 /* Pages.swift */; }; /* End PBXBuildFile section */ @@ -35,6 +36,7 @@ D5535842290F4BEA009E5D72 /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = ""; }; D5535844290F52F7009E5D72 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; D5535846290F5E6F009E5D72 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; + D5755A78291ADC00007F2201 /* Zoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zoom.swift; sourceTree = ""; }; D5AAF4042911C59E009743D3 /* PageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageView.swift; sourceTree = ""; }; D5AAF4062911C621009743D3 /* Pages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pages.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -82,6 +84,7 @@ D5AAF4042911C59E009743D3 /* PageView.swift */, D5535844290F52F7009E5D72 /* SettingsView.swift */, D553582C290E9718009E5D72 /* Swing.swift */, + D5755A78291ADC00007F2201 /* Zoom.swift */, D5535822290E9692009E5D72 /* Assets.xcassets */, D5535824290E9692009E5D72 /* Preview Content */, ); @@ -180,6 +183,7 @@ files = ( D5AAF4072911C621009743D3 /* Pages.swift in Sources */, D5535845290F52F7009E5D72 /* SettingsView.swift in Sources */, + D5755A79291ADC00007F2201 /* Zoom.swift in Sources */, D5AAF4052911C59E009743D3 /* PageView.swift in Sources */, D5535839290E9718009E5D72 /* AppDelegate.swift in Sources */, D5535847290F5E6F009E5D72 /* AppState.swift in Sources */, diff --git a/Demo/Demo/AppState.swift b/Demo/Demo/AppState.swift index 5611b59c..9b3667d3 100644 --- a/Demo/Demo/AppState.swift +++ b/Demo/Demo/AppState.swift @@ -9,6 +9,7 @@ final class AppState: ObservableObject { case slideAndFade case moveVertically case swing + case zoom var description: String { switch self { @@ -24,6 +25,8 @@ final class AppState: ObservableObject { return "Slide Vertically" case .swing: return "Swing" + case .zoom: + return "Zoom" } } @@ -41,6 +44,8 @@ final class AppState: ObservableObject { return .slide(axis: .vertical) case .swing: return .swing + case .zoom: + return .zoom } } } diff --git a/Demo/Demo/Swing.swift b/Demo/Demo/Swing.swift index 082ee8fb..615bc4ab 100644 --- a/Demo/Demo/Swing.swift +++ b/Demo/Demo/Swing.swift @@ -13,7 +13,7 @@ struct Swing: NavigationTransition { let offset: CGFloat = 150 Slide(axis: .horizontal) - Mirror { + MirrorPush { OnInsertion { Rotate(-angle) Offset(x: offset) diff --git a/Demo/Demo/Zoom.swift b/Demo/Demo/Zoom.swift new file mode 100644 index 00000000..90e45200 --- /dev/null +++ b/Demo/Demo/Zoom.swift @@ -0,0 +1,17 @@ +import NavigationTransition +import SwiftUI + +extension AnyNavigationTransition { + static var zoom: Self { + .init(Zoom()) + } +} + +struct Zoom: NavigationTransition { + var body: some NavigationTransition { + Slide(axis: .horizontal) + MirrorPush { + Scale(0.5) + } + } +} diff --git a/Sources/AtomicTransition/Group.swift b/Sources/AtomicTransition/Group.swift index 94d69a02..3684bf04 100644 --- a/Sources/AtomicTransition/Group.swift +++ b/Sources/AtomicTransition/Group.swift @@ -4,7 +4,7 @@ import class UIKit.UIView public struct Group: AtomicTransition { private let transitions: Transitions - init(_ transitions: Transitions) { + private init(_ transitions: Transitions) { self.transitions = transitions } diff --git a/Sources/AtomicTransition/Mirror.swift b/Sources/AtomicTransition/Mirror.swift new file mode 100644 index 00000000..dda9ae30 --- /dev/null +++ b/Sources/AtomicTransition/Mirror.swift @@ -0,0 +1,61 @@ +/// A transition that executes only on insertion. +public struct MirrorInsertion: AtomicTransition { + private let transition: Transition + + init(_ transition: Transition) { + self.transition = transition + } + + public init(@AtomicTransitionBuilder transition: () -> Transition) { + self.init(transition()) + } + + public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { + switch operation { + case .insertion: + transition.transition(view, for: operation, in: container) + case .removal: + return + } + } +} + +extension MirrorInsertion: MirrorableAtomicTransition where Transition: MirrorableAtomicTransition { + public func mirrored() -> MirrorRemoval { + .init(transition) + } +} + +extension MirrorInsertion: Equatable where Transition: Equatable {} +extension MirrorInsertion: Hashable where Transition: Hashable {} + +/// A transition that executes only on removal. +public struct MirrorRemoval: AtomicTransition { + private let transition: Transition + + init(_ transition: Transition) { + self.transition = transition + } + + public init(@AtomicTransitionBuilder transition: () -> Transition) { + self.init(transition()) + } + + public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { + switch operation { + case .insertion: + return + case .removal: + transition.transition(view, for: operation, in: container) + } + } +} + +extension MirrorRemoval: MirrorableAtomicTransition where Transition: MirrorableAtomicTransition { + public func mirrored() -> MirrorInsertion { + .init(transition) + } +} + +extension MirrorRemoval: Equatable where Transition: Equatable {} +extension MirrorRemoval: Hashable where Transition: Hashable {} diff --git a/Sources/NavigationTransition/Fade.swift b/Sources/NavigationTransition/Fade.swift index 835b4803..dfcfc8e3 100644 --- a/Sources/NavigationTransition/Fade.swift +++ b/Sources/NavigationTransition/Fade.swift @@ -24,7 +24,7 @@ public struct Fade: NavigationTransition { public var body: some NavigationTransition { switch style { case .in: - Mirror { + MirrorPush { OnInsertion { Opacity() } @@ -35,7 +35,7 @@ public struct Fade: NavigationTransition { } } case .out: - Mirror { + MirrorPush { OnRemoval { Opacity() } @@ -46,7 +46,7 @@ public struct Fade: NavigationTransition { } } case .cross: - Mirror { + MirrorPush { Opacity() } } diff --git a/Sources/NavigationTransition/Mirror.swift b/Sources/NavigationTransition/Mirror.swift index f07b039a..1b2a248e 100644 --- a/Sources/NavigationTransition/Mirror.swift +++ b/Sources/NavigationTransition/Mirror.swift @@ -1,8 +1,5 @@ import AtomicTransition -/// Typealias for `MirrorPush`. -public typealias Mirror = MirrorPush - public struct MirrorPush: NavigationTransition { private let transition: Transition diff --git a/Sources/NavigationTransition/Slide.swift b/Sources/NavigationTransition/Slide.swift index f505de7e..f73951f0 100644 --- a/Sources/NavigationTransition/Slide.swift +++ b/Sources/NavigationTransition/Slide.swift @@ -41,7 +41,7 @@ public struct Slide: NavigationTransition { public var body: some NavigationTransition { switch axis { case .horizontal: - Mirror { + MirrorPush { OnInsertion { Move(edge: .trailing) } @@ -50,7 +50,7 @@ public struct Slide: NavigationTransition { } } case .vertical: - Mirror { + MirrorPush { OnInsertion { Move(edge: .bottom) } From 2df8cb1d9bd8c4e6f637f60348941ecd6a47d3e9 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:00:46 +0000 Subject: [PATCH 07/16] wip --- Sources/AtomicTransition/Asymmetric.swift | 36 ----------------------- 1 file changed, 36 deletions(-) diff --git a/Sources/AtomicTransition/Asymmetric.swift b/Sources/AtomicTransition/Asymmetric.swift index 5d1a6d7a..02633fe5 100644 --- a/Sources/AtomicTransition/Asymmetric.swift +++ b/Sources/AtomicTransition/Asymmetric.swift @@ -1,41 +1,5 @@ import class UIKit.UIView -/// A composite transition that uses a different transition for insertion versus removal. -public struct Asymmetric: AtomicTransition { - private let insertion: InsertionTransition - private let removal: RemovalTransition - - init(insertion: InsertionTransition, removal: RemovalTransition) { - self.insertion = insertion - self.removal = removal - } - - public init( - @AtomicTransitionBuilder insertion: () -> InsertionTransition, - @AtomicTransitionBuilder removal: () -> RemovalTransition - ) { - self.init(insertion: insertion(), removal: removal()) - } - - public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { - switch operation { - case .insertion: - insertion.transition(view, for: operation, in: container) - case .removal: - removal.transition(view, for: operation, in: container) - } - } -} - -extension Asymmetric: MirrorableAtomicTransition where InsertionTransition: MirrorableAtomicTransition, RemovalTransition: MirrorableAtomicTransition { - public func mirrored() -> Asymmetric { - return .init(insertion: insertion.mirrored(), removal: removal.mirrored()) - } -} - -extension Asymmetric: Equatable where InsertionTransition: Equatable, RemovalTransition: Equatable {} -extension Asymmetric: Hashable where InsertionTransition: Hashable, RemovalTransition: Hashable {} - /// A transition that executes only on insertion. public struct OnInsertion: AtomicTransition { private let transition: Transition From 6e72e09bfc50454b777e46d6b99749248447f134 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:22:43 +0000 Subject: [PATCH 08/16] wip --- Demo/Demo/Swing.swift | 14 ++++---------- Sources/AtomicTransition/AtomicTransition.swift | 12 ++++++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Demo/Demo/Swing.swift b/Demo/Demo/Swing.swift index 615bc4ab..bda8ac97 100644 --- a/Demo/Demo/Swing.swift +++ b/Demo/Demo/Swing.swift @@ -9,19 +9,19 @@ extension AnyNavigationTransition { struct Swing: NavigationTransition { var body: some NavigationTransition { - let angle = Angle(degrees: 70) - let offset: CGFloat = 150 + let angle = 70.0 + let offset = 150.0 Slide(axis: .horizontal) MirrorPush { OnInsertion { - Rotate(-angle) + Rotate(.degrees(-angle)) Offset(x: offset) Opacity() Scale(0.5) } OnRemoval { - Rotate(angle) + Rotate(.degrees(angle)) Offset(x: -offset) } } @@ -32,9 +32,3 @@ struct Swing: NavigationTransition { } } } - -extension Angle { - static prefix func - (_ rhs: Self) -> Self { - .init(degrees: -rhs.degrees) - } -} diff --git a/Sources/AtomicTransition/AtomicTransition.swift b/Sources/AtomicTransition/AtomicTransition.swift index 33eda4e0..a6576303 100644 --- a/Sources/AtomicTransition/AtomicTransition.swift +++ b/Sources/AtomicTransition/AtomicTransition.swift @@ -31,8 +31,20 @@ public enum AtomicTransitionOperation { case removal } +/// Defines an `AtomicTransition` that can be mirrored. It is a specialized building block of `NavigationTransition`. +/// +/// A transition that conform to these protocol expose a `Mirrored` associated type expressing the type resulting +/// from mirroring the transition. public protocol MirrorableAtomicTransition: AtomicTransition { associatedtype Mirrored: AtomicTransition + /// The mirrored transition. + /// + /// > Note: A good indicator of a proper implementation for this function is that it should round-trip + /// > to its original value when called twice: + /// > + /// > ```swift + /// > Offset(x: 10).mirrored().mirrored() == Offset(x: 10) + /// > ``` func mirrored() -> Mirrored } From 802212578e66d5220bc747ad65e5f34a07f021da Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:23:03 +0000 Subject: [PATCH 09/16] wip --- Demo/Demo/Swing.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Demo/Demo/Swing.swift b/Demo/Demo/Swing.swift index bda8ac97..8487a112 100644 --- a/Demo/Demo/Swing.swift +++ b/Demo/Demo/Swing.swift @@ -9,11 +9,10 @@ extension AnyNavigationTransition { struct Swing: NavigationTransition { var body: some NavigationTransition { - let angle = 70.0 - let offset = 150.0 - Slide(axis: .horizontal) MirrorPush { + let angle = 70.0 + let offset = 150.0 OnInsertion { Rotate(.degrees(-angle)) Offset(x: offset) From cc262ea3a865e80c6a5c3808df313b850de2abd8 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:31:26 +0000 Subject: [PATCH 10/16] wip --- Sources/AtomicTransition/Asymmetric.swift | 2 +- Sources/AtomicTransition/Mirror.swift | 4 ++-- Sources/NavigationTransition/Erased.swift | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/AtomicTransition/Asymmetric.swift b/Sources/AtomicTransition/Asymmetric.swift index 02633fe5..013534b0 100644 --- a/Sources/AtomicTransition/Asymmetric.swift +++ b/Sources/AtomicTransition/Asymmetric.swift @@ -4,7 +4,7 @@ import class UIKit.UIView public struct OnInsertion: AtomicTransition { private let transition: Transition - init(_ transition: Transition) { + private init(_ transition: Transition) { self.transition = transition } diff --git a/Sources/AtomicTransition/Mirror.swift b/Sources/AtomicTransition/Mirror.swift index dda9ae30..6815ffca 100644 --- a/Sources/AtomicTransition/Mirror.swift +++ b/Sources/AtomicTransition/Mirror.swift @@ -2,7 +2,7 @@ public struct MirrorInsertion: AtomicTransition { private let transition: Transition - init(_ transition: Transition) { + fileprivate init(_ transition: Transition) { self.transition = transition } @@ -33,7 +33,7 @@ extension MirrorInsertion: Hashable where Transition: Hashable {} public struct MirrorRemoval: AtomicTransition { private let transition: Transition - init(_ transition: Transition) { + fileprivate init(_ transition: Transition) { self.transition = transition } diff --git a/Sources/NavigationTransition/Erased.swift b/Sources/NavigationTransition/Erased.swift index 6da74ab7..144930bf 100644 --- a/Sources/NavigationTransition/Erased.swift +++ b/Sources/NavigationTransition/Erased.swift @@ -6,6 +6,7 @@ struct Erased: NavigationTransition { self.handler = handler } + @inlinable func transition( from fromView: TransientView, to toView: TransientView, From 53070086058cf01e737ad9e2d54022be800a6fc0 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:37:25 +0000 Subject: [PATCH 11/16] wip --- Sources/AtomicTransition/Asymmetric.swift | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Sources/AtomicTransition/Asymmetric.swift b/Sources/AtomicTransition/Asymmetric.swift index 013534b0..56147736 100644 --- a/Sources/AtomicTransition/Asymmetric.swift +++ b/Sources/AtomicTransition/Asymmetric.swift @@ -1,5 +1,41 @@ import class UIKit.UIView +/// A composite transition that uses a different transition for push versus pop. +public struct Asymmetric: AtomicTransition { + private let insertion: InsertionTransition + private let removal: RemovalTransition + + init(insertion: InsertionTransition, removal: RemovalTransition) { + self.insertion = insertion + self.removal = removal + } + + public init( + @AtomicTransitionBuilder insertion: () -> InsertionTransition, + @AtomicTransitionBuilder removal: () -> RemovalTransition + ) { + self.init(insertion: insertion(), removal: removal()) + } + + public func transition(_ view: TransientView, for operation: TransitionOperation, in container: Container) { + switch operation { + case .insertion: + insertion.transition(view, for: operation, in: container) + case .removal: + removal.transition(view, for: operation, in: container) + } + } +} + +extension Asymmetric: MirrorableAtomicTransition where InsertionTransition: MirrorableAtomicTransition, RemovalTransition: MirrorableAtomicTransition { + public func mirrored() -> Asymmetric { + return .init(insertion: insertion.mirrored(), removal: removal.mirrored()) + } +} + +extension Asymmetric: Equatable where InsertionTransition: Equatable, RemovalTransition: Equatable {} +extension Asymmetric: Hashable where InsertionTransition: Hashable, RemovalTransition: Hashable {} + /// A transition that executes only on insertion. public struct OnInsertion: AtomicTransition { private let transition: Transition From 4fafb29b3d7096c77ede1047b5bdfac579bb7521 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:38:02 +0000 Subject: [PATCH 12/16] wip --- Sources/AtomicTransition/Asymmetric.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AtomicTransition/Asymmetric.swift b/Sources/AtomicTransition/Asymmetric.swift index 56147736..4d834e8b 100644 --- a/Sources/AtomicTransition/Asymmetric.swift +++ b/Sources/AtomicTransition/Asymmetric.swift @@ -5,7 +5,7 @@ public struct Asymmetric Date: Tue, 8 Nov 2022 20:39:06 +0000 Subject: [PATCH 13/16] wip --- Sources/AtomicTransition/Identity.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AtomicTransition/Identity.swift b/Sources/AtomicTransition/Identity.swift index 3845bfc7..6e67871f 100644 --- a/Sources/AtomicTransition/Identity.swift +++ b/Sources/AtomicTransition/Identity.swift @@ -8,7 +8,7 @@ public struct Identity: AtomicTransition, MirrorableAtomicTransition { // NO-OP } - @_transparent + @inlinable public func mirrored() -> Self { self } From 975797bba121fb64d19d8e12057a94c4a2e0c1e4 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:41:15 +0000 Subject: [PATCH 14/16] wip --- README.md | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index eca8ab64..c8acb302 100644 --- a/README.md +++ b/README.md @@ -96,34 +96,24 @@ In addition to these, you can create fully [**custom**](Demo/Demo/Swing.swift) t ```swift struct Swing: NavigationTransition { var body: some NavigationTransition { - let angle = Angle(degrees: 70) - let offset: CGFloat = 150 - let scale: CGFloat = 0.5 - Slide(axis: .horizontal) - OnPush { + MirrorPush { + let angle = 70.0 + let offset = 150.0 OnInsertion { - Rotate(-angle) + Rotate(.degrees(-angle)) Offset(x: offset) Opacity() - Scale(scale) + Scale(0.5) } OnRemoval { - Rotate(angle) + Rotate(.degrees(angle)) Offset(x: -offset) } } OnPop { - OnInsertion { - Rotate(angle) - Offset(x: -offset) - Opacity() - Scale(scale) - BringToFront() - } OnRemoval { - Rotate(-angle) - Offset(x: offset) + SendToBack() } } } From 70eb8752ef26e9979fa87b032f870f9a90bebec3 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:50:46 +0000 Subject: [PATCH 15/16] wip --- Documentation/Custom-Transitions.md | 30 ++++++++++------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/Documentation/Custom-Transitions.md b/Documentation/Custom-Transitions.md index 9995d00a..c085b8e0 100644 --- a/Documentation/Custom-Transitions.md +++ b/Documentation/Custom-Transitions.md @@ -47,7 +47,7 @@ public struct Slide: NavigationTransition { public var body: some NavigationTransition { switch axis { case .horizontal: - OnPush { + MirrorPush { OnInsertion { Move(edge: .trailing) } @@ -55,16 +55,8 @@ public struct Slide: NavigationTransition { Move(edge: .leading) } } - OnPop { - OnInsertion { - Move(edge: .leading) - } - OnRemoval { - Move(edge: .trailing) - } - } case .vertical: - OnPush { + MirrorPush { OnInsertion { Move(edge: .bottom) } @@ -72,14 +64,6 @@ public struct Slide: NavigationTransition { Move(edge: .top) } } - OnPop { - OnInsertion { - Move(edge: .top) - } - OnRemoval { - Move(edge: .bottom) - } - } } } } @@ -229,7 +213,13 @@ Next up, let's explore two ways of conforming to `NavigationTransition`. 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). -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`: +Check out the documentation to learn about the different `NavigationTransition` types and how they compose. + +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. + +--- + +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`: ```swift 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 - `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. - `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. -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. +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. ### UIKit From be44ddae0946862a21e77943389ca69f59331b38 Mon Sep 17 00:00:00 2001 From: David Roman <2538074+davdroman@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:53:11 +0000 Subject: [PATCH 16/16] wip --- Documentation/Custom-Transitions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/Custom-Transitions.md b/Documentation/Custom-Transitions.md index c085b8e0..19c5bf51 100644 --- a/Documentation/Custom-Transitions.md +++ b/Documentation/Custom-Transitions.md @@ -211,9 +211,9 @@ All types conforming to `AtomicTransition` must implement what's known as a "tra Next up, let's explore two ways of conforming to `NavigationTransition`. -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). +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). -Check out the documentation to learn about the different `NavigationTransition` types and how they compose. +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. 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.