From 9521b88d9cd03803ec42063c57c002941962a6dc Mon Sep 17 00:00:00 2001 From: Erik Drobne Date: Thu, 9 Nov 2023 14:10:05 +0100 Subject: [PATCH 1/2] Add modal transition delegate associated value to the present action. --- .../project.pbxproj | 4 ++ .../SimpleShapes/SimpleShapesRoute.swift | 7 ++- .../Transitions/SlideTransition.swift | 52 +++++++++++++++++++ .../Navigator/Navigator.swift | 12 ++++- .../Transition/TransitionAction.swift | 3 +- 5 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Transitions/SlideTransition.swift diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample.xcodeproj/project.pbxproj b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample.xcodeproj/project.pbxproj index d233fec..08ced8f 100644 --- a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample.xcodeproj/project.pbxproj +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 17AABAF12A6D5F2100AFE8A7 /* ShapesAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AABAF02A6D5F2100AFE8A7 /* ShapesAction.swift */; }; 17C379712ACEDD7800CA4105 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C379702ACEDD7800CA4105 /* AppCoordinator.swift */; }; 17C379742ACEE1EA00CA4105 /* CoordinatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C379732ACEE1EA00CA4105 /* CoordinatorFactory.swift */; }; + 17DF24AF2AFD14C600578CD9 /* SlideTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DF24AE2AFD14C600578CD9 /* SlideTransition.swift */; }; 17F1183529CC63B1004755DB /* SimpleShapesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F1183429CC63B1004755DB /* SimpleShapesView.swift */; }; 17F1183729CC63C0004755DB /* CustomShapesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F1183629CC63C0004755DB /* CustomShapesView.swift */; }; 17F1183B29CC6678004755DB /* SimpleShapesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F1183A29CC6678004755DB /* SimpleShapesCoordinator.swift */; }; @@ -50,6 +51,7 @@ 17AABAF02A6D5F2100AFE8A7 /* ShapesAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesAction.swift; sourceTree = ""; }; 17C379702ACEDD7800CA4105 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 17C379732ACEE1EA00CA4105 /* CoordinatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorFactory.swift; sourceTree = ""; }; + 17DF24AE2AFD14C600578CD9 /* SlideTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlideTransition.swift; sourceTree = ""; }; 17E1C1FF2A1BD86D00542AB9 /* SwiftUICoordinator.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SwiftUICoordinator.xctestplan; sourceTree = ""; }; 17F1183429CC63B1004755DB /* SimpleShapesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleShapesView.swift; sourceTree = ""; }; 17F1183629CC63C0004755DB /* CustomShapesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomShapesView.swift; sourceTree = ""; }; @@ -122,6 +124,7 @@ isa = PBXGroup; children = ( 17360A402A1275D600DB2296 /* FadeTransition.swift */, + 17DF24AE2AFD14C600578CD9 /* SlideTransition.swift */, ); path = Transitions; sourceTree = ""; @@ -297,6 +300,7 @@ 17C379742ACEE1EA00CA4105 /* CoordinatorFactory.swift in Sources */, 17AABAF12A6D5F2100AFE8A7 /* ShapesAction.swift in Sources */, 176F3CB829B8C05B009C4987 /* ShapesCoordinator.swift in Sources */, + 17DF24AF2AFD14C600578CD9 /* SlideTransition.swift in Sources */, 176F3CB529B8BF71009C4987 /* ShapeListView.swift in Sources */, 17C379712ACEDD7800CA4105 /* AppCoordinator.swift in Sources */, 17360A412A1275D600DB2296 /* FadeTransition.swift in Sources */, diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/SimpleShapes/SimpleShapesRoute.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/SimpleShapes/SimpleShapesRoute.swift index 62be4af..186a9e6 100644 --- a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/SimpleShapes/SimpleShapesRoute.swift +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Coordinators/SimpleShapes/SimpleShapesRoute.swift @@ -34,6 +34,11 @@ enum SimpleShapesRoute: NavigationRoute { } var action: TransitionAction? { - return .push(animated: true) + switch self { + case .rect: + return .present(delegate: SlideTransitionDelegate()) + default: + return .push(animated: true) + } } } diff --git a/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Transitions/SlideTransition.swift b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Transitions/SlideTransition.swift new file mode 100644 index 0000000..db454aa --- /dev/null +++ b/Example/SwiftUICoordinatorExample/SwiftUICoordinatorExample/Transitions/SlideTransition.swift @@ -0,0 +1,52 @@ +// +// SlideTransition.swift +// SwiftUICoordinatorExample +// +// Created by Erik Drobne on 9. 11. 23. +// + +import UIKit + +class SlideTransition: NSObject, UIViewControllerAnimatedTransitioning { + let isPresenting: Bool + + init(isPresenting: Bool) { + self.isPresenting = isPresenting + super.init() + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + 0.5 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + let key = isPresenting ? UITransitionContextViewControllerKey.to : UITransitionContextViewControllerKey.from + guard let controller = transitionContext.viewController(forKey: key) else { return } + + if isPresenting { + transitionContext.containerView.addSubview(controller.view) + } + + let finalFrame = transitionContext.finalFrame(for: controller) + let startingFrame = isPresenting ? finalFrame.offsetBy(dx: 0, dy: -finalFrame.height) : finalFrame + let endingFrame = isPresenting ? finalFrame : finalFrame.offsetBy(dx: 0, dy: -finalFrame.height) + + controller.view.frame = startingFrame + + UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: { + controller.view.frame = endingFrame + }) { _ in + transitionContext.completeTransition(!transitionContext.transitionWasCancelled) + } + } +} + +final class SlideTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { + func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SlideTransition(isPresenting: true) + } + + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SlideTransition(isPresenting: false) + } +} diff --git a/Sources/SwiftUICoordinator/Navigator/Navigator.swift b/Sources/SwiftUICoordinator/Navigator/Navigator.swift index dfd4b44..52a0210 100644 --- a/Sources/SwiftUICoordinator/Navigator/Navigator.swift +++ b/Sources/SwiftUICoordinator/Navigator/Navigator.swift @@ -65,11 +65,12 @@ public extension Navigator where Self: RouterViewFactory { switch route.action { case .push(let animated): navigationController.pushViewController(viewController, animated: animated) - case .present(let animated, let modalPresentationStyle, let completion): + case .present(let animated, let modalPresentationStyle, let delegate, let completion): present( viewController: viewController, animated: animated, modalPresentationStyle: modalPresentationStyle, + delegate: delegate, completion: completion ) case .none: @@ -129,9 +130,16 @@ public extension Navigator where Self: RouterViewFactory { viewController: UIViewController, animated: Bool, modalPresentationStyle: UIModalPresentationStyle, + delegate: UIViewControllerTransitioningDelegate?, completion: (() -> Void)? ) { - viewController.modalPresentationStyle = modalPresentationStyle + if let delegate { + viewController.modalPresentationStyle = .custom + viewController.transitioningDelegate = delegate + } else { + viewController.modalPresentationStyle = modalPresentationStyle + } + navigationController.present(viewController, animated: animated, completion: completion) } } diff --git a/Sources/SwiftUICoordinator/Transition/TransitionAction.swift b/Sources/SwiftUICoordinator/Transition/TransitionAction.swift index 116a4d4..c6a13f3 100644 --- a/Sources/SwiftUICoordinator/Transition/TransitionAction.swift +++ b/Sources/SwiftUICoordinator/Transition/TransitionAction.swift @@ -12,8 +12,9 @@ public enum TransitionAction { case push(animated: Bool) /// Represents a present action for presenting a view controller modally. case present( - animated: Bool, + animated: Bool = true, modalPresentationStyle: UIModalPresentationStyle = .automatic, + delegate: UIViewControllerTransitioningDelegate? = nil, completion: (() -> Void)? = nil ) } From c5ab7ccbe9f2315e566c086c32605c4583c6060e Mon Sep 17 00:00:00 2001 From: Erik Drobne Date: Mon, 13 Nov 2023 11:08:23 +0100 Subject: [PATCH 2/2] Update README.md --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index b066991..2c444b3 100644 --- a/README.md +++ b/README.md @@ -365,6 +365,39 @@ lazy var delegate = factory.makeNavigationDelegate([FadeTransition()]) lazy var navigationController = factory.makeNavigationController(delegate: delegate) ``` +#### Modal transitions + +Custom modal transitions can enhance the user experience by providing a unique way to `present` and `dismiss` view controllers. + +First, define a transition delegate object that conforms to the `UIViewControllerTransitioningDelegate` protocol. + +```Swift +final class SlideTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { + func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SlideTransition(isPresenting: true) + } + + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return SlideTransition(isPresenting: false) + } +} +``` + +In this example, `SlideTransition` is a custom class that conforms to the `UIViewControllerAnimatedTransitioning` protocol and handles the actual animation logic. + +Pass the `SlideTransitionDelegate` instance to the specific action where you wish to apply your modal transition. + +```Swift +var action: TransitionAction? { + switch self { + case .rect: + return .present(delegate: SlideTransitionDelegate()) + default: + return .push(animated: true) + } +} +``` + ### Handling deep links In your application, you can handle deep links by creating a `DeepLinkHandler` that conforms to the `DeepLinkHandling` protocol. This handler will specify the URL scheme and the supported deep links that your app can recognize.