Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/custom transitions #11

Merged
merged 17 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
1732C9FC29A6607500C2BC1F /* SwiftUICoordinatorExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1732C9FB29A6607500C2BC1F /* SwiftUICoordinatorExampleApp.swift */; };
1732CA0029A6607800C2BC1F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1732C9FF29A6607800C2BC1F /* Assets.xcassets */; };
1732CA0329A6607800C2BC1F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1732CA0229A6607800C2BC1F /* Preview Assets.xcassets */; };
17360A412A1275D600DB2296 /* FadeTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17360A402A1275D600DB2296 /* FadeTransition.swift */; };
176F3CB529B8BF71009C4987 /* ShapesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176F3CB429B8BF71009C4987 /* ShapesView.swift */; };
176F3CB829B8C05B009C4987 /* ShapesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176F3CB729B8C05B009C4987 /* ShapesCoordinator.swift */; };
176F3CBB29B8C162009C4987 /* ShapesRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176F3CBA29B8C162009C4987 /* ShapesRoute.swift */; };
Expand All @@ -30,6 +31,7 @@
1732C9FB29A6607500C2BC1F /* SwiftUICoordinatorExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUICoordinatorExampleApp.swift; sourceTree = "<group>"; };
1732C9FF29A6607800C2BC1F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
1732CA0229A6607800C2BC1F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
17360A402A1275D600DB2296 /* FadeTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FadeTransition.swift; sourceTree = "<group>"; };
176F3CB429B8BF71009C4987 /* ShapesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesView.swift; sourceTree = "<group>"; };
176F3CB729B8C05B009C4987 /* ShapesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesCoordinator.swift; sourceTree = "<group>"; };
176F3CBA29B8C162009C4987 /* ShapesRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapesRoute.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -81,6 +83,7 @@
1732C9FB29A6607500C2BC1F /* SwiftUICoordinatorExampleApp.swift */,
176F3CB629B8C01E009C4987 /* Coordinators */,
176F3CB329B8BD92009C4987 /* Views */,
17360A3F2A1275C000DB2296 /* Transitions */,
1732C9FF29A6607800C2BC1F /* Assets.xcassets */,
1732CA0129A6607800C2BC1F /* Preview Content */,
);
Expand All @@ -95,6 +98,14 @@
path = "Preview Content";
sourceTree = "<group>";
};
17360A3F2A1275C000DB2296 /* Transitions */ = {
isa = PBXGroup;
children = (
17360A402A1275D600DB2296 /* FadeTransition.swift */,
);
path = Transitions;
sourceTree = "<group>";
};
176F3CB329B8BD92009C4987 /* Views */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -241,6 +252,7 @@
17F1184529CC8CF6004755DB /* Star.swift in Sources */,
176F3CB829B8C05B009C4987 /* ShapesCoordinator.swift in Sources */,
176F3CB529B8BF71009C4987 /* ShapesView.swift in Sources */,
17360A412A1275D600DB2296 /* FadeTransition.swift in Sources */,
17F1183D29CC668F004755DB /* SimpleShapesRoute.swift in Sources */,
17F1183529CC63B1004755DB /* SimpleShapesView.swift in Sources */,
176F3CBB29B8C162009C4987 /* ShapesRoute.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ class CustomShapesCoordinator: NSObject, Coordinator, Navigator {

weak var parent: Coordinator? = nil
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
var navigationController: NavigationController
let startRoute: CustomShapesRoute?

// MARK: - Initialization

init(parent: Coordinator?, navigationController: UINavigationController = .init(), startRoute: CustomShapesRoute? = .customShapes) {
init(parent: Coordinator?, navigationController: NavigationController, startRoute: CustomShapesRoute? = .customShapes) {
self.parent = parent
self.navigationController = navigationController
self.startRoute = startRoute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ enum CustomShapesRoute: NavigationRoute {
}
}

var transition: NavigationTransition? {
return .push()
var action: TransitionAction? {
return .push(animated: true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ class ShapesCoordinator: NSObject, Coordinator, Navigator {
// MARK: - Internal properties

/// Root coordinator doesn't have a parent.
weak var parent: Coordinator? = nil
let parent: Coordinator? = nil
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
var navigationController: NavigationController
let startRoute: ShapesRoute?

// MARK: - Initialization

init(navigationController: UINavigationController = .init(), startRoute: ShapesRoute? = nil) {
self.navigationController = navigationController
init(startRoute: ShapesRoute? = nil) {
self.navigationController = NavigationController()
self.startRoute = startRoute
super.init()

setup()
}

func navigate(to route: NavigationRoute) {
Expand Down Expand Up @@ -51,6 +53,10 @@ class ShapesCoordinator: NSObject, Coordinator, Navigator {
}

// MARK: - Private methods

private func setup() {
navigationController.register(FadeTransition())
}

private func makeSimpleShapesCoordinator() -> SimpleShapesCoordinator {
let coordinator = SimpleShapesCoordinator(parent: self, navigationController: navigationController)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ enum ShapesRoute: NavigationRoute {
}
}

var transition: NavigationTransition? {
var action: TransitionAction? {
switch self {
case .simpleShapes:
/// We have to pass nil for the route presenting a child coordinator.
return nil
default:
return .push()
return .push(animated: true)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ class SimpleShapesCoordinator: NSObject, Coordinator, Navigator {

weak var parent: Coordinator? = nil
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
var navigationController: NavigationController
let startRoute: SimpleShapesRoute?

// MARK: - Initialization

init(parent: Coordinator?, navigationController: UINavigationController = .init(), startRoute: SimpleShapesRoute? = .simpleShapes) {
init(parent: Coordinator?, navigationController: NavigationController, startRoute: SimpleShapesRoute? = .simpleShapes) {
self.parent = parent
self.navigationController = navigationController
self.startRoute = startRoute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ enum SimpleShapesRoute: NavigationRoute {
}
}

var transition: NavigationTransition? {
return .push()
var action: TransitionAction? {
return .push(animated: true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// FadeTransition.swift
// SwiftUICoordinatorExample
//
// Created by Erik Drobne on 15/05/2023.
//

import SwiftUI
import SwiftUICoordinator

class FadeTransition: NSObject, Transition {
func isEligible(from fromRoute: NavigationRoute, to toRoute: NavigationRoute) -> Bool {
return (fromRoute as? CustomShapesRoute == .customShapes && toRoute as? CustomShapesRoute == .star)
}

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3 // Set the duration of the fade animation
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to) else {
transitionContext.completeTransition(false)
return
}

let containerView = transitionContext.containerView
toView.alpha = 0.0

containerView.addSubview(toView)

UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toView.alpha = 1.0
}) { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ extension CustomShapesView {
}

struct CustomShapesView_Previews: PreviewProvider {
static let coordinator = CustomShapesCoordinator(parent: nil)
static let coordinator = CustomShapesCoordinator(parent: nil, navigationController: .init())

static var previews: some View {
CustomShapesView<CustomShapesCoordinator>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ extension SimpleShapesView {
}

struct SimpleShapesView_Previews: PreviewProvider {
static let coordinator = SimpleShapesCoordinator(parent: nil)
static let coordinator = SimpleShapesCoordinator(parent: nil, navigationController: .init())

static var previews: some View {
SimpleShapesView<SimpleShapesCoordinator>()
Expand Down
58 changes: 52 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ This protocol defines the available routes for navigation within a coordinator f
public protocol NavigationRoute {
/// This title can be used to set the navigation bar title when the route is shown.
var title: String? { get }
/// The type of transition to be used when the route is shown.
/// This can be a push transition, a modal presentation, or `nil` (for child coordinators).
var transition: NavigationTransition? { get }
/// Transition action to be used when the route is shown.
/// This can be a push action, a modal presentation, or `nil` (for child coordinators).
var action: TransitionAction? { get }
}
```

Expand All @@ -78,7 +78,7 @@ public protocol Navigator: ObservableObject {
/// This method is called when the navigator should start navigating.
func start() throws
/// Navigate to a specific route.
/// It creates a view for the route and adds it to the navigation stack using the specified transition.
/// It creates a view for the route and adds it to the navigation stack using the specified action (TransitionAction).
func show(route: Route) throws
/// Sets the navigation stack to a new array of routes.
/// It can be useful if you need to reset the entire navigation stack to a new set of views.
Expand Down Expand Up @@ -140,7 +140,7 @@ enum ShapesRoute: NavigationRoute {
}
}

var transition: NavigationTransition? {
var action: TransitionAction? {
switch self {
case .simpleShapes:
// We have to pass nil for the route presenting a child coordinator.
Expand All @@ -162,7 +162,7 @@ class ShapesCoordinator: NSObject, Coordinator, Navigator {
// MARK: - Internal properties

/// Root coordinator doesn't have a parent.
weak var parent: Coordinator? = nil
let parent: Coordinator? = nil
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
let startRoute: ShapesRoute?
Expand Down Expand Up @@ -293,6 +293,52 @@ struct ShapesView<Coordinator: Routing>: View {
}
```

### Custom transitions

Create custom transition.

```Swift
class FadeTransition: NSObject, Transition {
func isEligible(from fromRoute: NavigationRoute, to toRoute: NavigationRoute) -> Bool {
return (fromRoute as? CustomShapesRoute == .customShapes && toRoute as? CustomShapesRoute == .star)
}

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3 // Set the duration of the fade animation
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to) else {
transitionContext.completeTransition(false)
return
}

let containerView = transitionContext.containerView
toView.alpha = 0.0

containerView.addSubview(toView)

UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toView.alpha = 1.0
}) { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
```

Register transition in the coordinator initializer.

```Swift
init(startRoute: ShapesRoute? = nil) {
self.navigationController = NavigationController()
self.startRoute = startRoute
super.init()

navigationController.register(FadeTransition())
}
```

## 📒 Example project

For better understanding, I recommend that you take a look at the example project located in the `SwiftUICoordinatorExample` folder.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Erik Drobne on 12/12/2022.
//

import Foundation
erikdrobne marked this conversation as resolved.
Show resolved Hide resolved
import SwiftUI

@MainActor
Expand All @@ -17,7 +18,12 @@ public protocol Coordinator: AnyObject {
func finish()
}

// MARK: - Extensions

public extension Coordinator {

// MARK: - Public methods

func finish() {
parent?.childCoordinators.removeAll(where: { $0 === self })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Erik Drobne on 12/12/2022.
//

import Foundation
erikdrobne marked this conversation as resolved.
Show resolved Hide resolved
import SwiftUI

public typealias Routing = Coordinator & Navigator
Expand All @@ -13,7 +14,7 @@ public typealias Routing = Coordinator & Navigator
public protocol Navigator: ObservableObject {
associatedtype Route: NavigationRoute

var navigationController: UINavigationController { get set }
var navigationController: NavigationController { get set }
var startRoute: Route? { get }

func start() throws
Expand All @@ -25,7 +26,11 @@ public protocol Navigator: ObservableObject {
func dismiss(animated: Bool)
}

// MARK: - Extensions

public extension Navigator where Self: Coordinator, Self: RouterViewFactory {

// MARK: - Public properties

var viewControllers: [UIViewController] {
return navigationController.viewControllers
Expand All @@ -38,6 +43,8 @@ public extension Navigator where Self: Coordinator, Self: RouterViewFactory {
var visibleViewController: UIViewController? {
return navigationController.visibleViewController
}

// MARK: - Public methods

func start() throws {
guard let route = startRoute else {
Expand All @@ -54,9 +61,12 @@ public extension Navigator where Self: Coordinator, Self: RouterViewFactory {
}

let viewWithCoordinator = view.environmentObject(self)
let viewController = UIHostingController(rootView: viewWithCoordinator)
let viewController = RouteHostingController(
rootView: viewWithCoordinator,
route: route
)

switch route.transition {
switch route.action {
case .push(let animated):
navigationController.pushViewController(viewController, animated: animated)
case .present(let animated, let modalPresentationStyle, let completion):
Expand Down Expand Up @@ -98,7 +108,7 @@ public extension Navigator where Self: Coordinator, Self: RouterViewFactory {
let view = self.view(for: route).ifLet(route.title) { view, value in
view.navigationTitle(value)
}
return UIHostingController(rootView: view.environmentObject(self))
return RouteHostingController(rootView: view.environmentObject(self), route: route)
})
}

Expand Down
Loading