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/transition provider #25

Merged
merged 5 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -11,7 +11,10 @@ import SwiftUICoordinator
@MainActor
final class DependencyContainer {

let navigationController = NavigationController()
lazy var transitionProvider = TransitionProvider(
transitions: [FadeTransition()]
)
lazy var navigationController = NavigationController(transitionProvider: transitionProvider)
let deepLinkHandler = DeepLinkHandler.shared

private(set) var appCoordinator: AppCoordinator?
Expand All @@ -29,8 +32,7 @@ extension DependencyContainer: CoordinatorFactory {
func makeAppCoordinator(window: UIWindow) -> AppCoordinator {
return AppCoordinator(
window: window,
navigationController: self.navigationController,
transitions: [FadeTransition()]
navigationController: self.navigationController
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import UIKit
import SwiftUICoordinator

class FadeTransition: NSObject, Transition {
class FadeTransition: NSObject, Transitionable {
func isEligible(from fromRoute: NavigationRoute, to toRoute: NavigationRoute, operation: NavigationOperation) -> Bool {
return (fromRoute as? CustomShapesRoute == .customShapes && toRoute as? CustomShapesRoute == .star)
}
Expand Down
5 changes: 1 addition & 4 deletions Sources/SwiftUICoordinator/Coordinator/RootCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,14 @@ open class RootCoordinator: Coordinator {
/// RootCoordinator doesn't have a parent
public let parent: Coordinator? = nil
public var childCoordinators = [WeakCoordinator]()
private let transitions: [Transition]

public private(set) var window: UIWindow
public private(set) var navigationController: NavigationController

public init(window: UIWindow, navigationController: NavigationController, transitions: [Transition] = []) {
public init(window: UIWindow, navigationController: NavigationController) {
self.window = window
self.navigationController = navigationController
self.transitions = transitions

navigationController.register(transitions)
window.rootViewController = self.navigationController
window.makeKeyAndVisible()
}
Expand Down
56 changes: 13 additions & 43 deletions Sources/SwiftUICoordinator/Routing/NavigationController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import SwiftUI
@MainActor
public class NavigationController: UINavigationController {

// MARK: - Internal Properties

/// The collection of registered transition objects.
private(set) var transitions = [WeakTransition]()
public private(set) var transitionProvider: TransitionProvidable?

// MARK: - Initialization

Expand All @@ -24,10 +21,11 @@ public class NavigationController: UINavigationController {
///
/// - Note: By default `isNavigationBarHidden` is set to `true`.
///
public convenience init(isNavigationBarHidden: Bool = true) {
public convenience init(isNavigationBarHidden: Bool = true, transitionProvider: TransitionProvidable? = nil) {
self.init(nibName: nil, bundle: nil)

self.isNavigationBarHidden = isNavigationBarHidden
self.transitionProvider = transitionProvider
}

private override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
Expand All @@ -39,34 +37,6 @@ public class NavigationController: UINavigationController {
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Public methods

/// Registers a single `Transition` for use in navigation animations.
///
/// - Parameter transition: The `Transition` to be registered.
public func register(_ transition: Transition) {
transitions.append(WeakTransition(transition))
}

/// Registers multiple `Transition` objects for use in navigation animations.
///
/// - Parameter transitions: An array of `Transition` objects to be registered.
public func register(_ transitions: [Transition]) {
self.transitions += transitions.map { WeakTransition($0) }
}

/// Unregisters all `Transition` instances of the specified type.
///
/// - Parameter type: The type of `Transition` to be unregistered.
public func unregister<T: Transition>(_ type: T.Type) {
transitions.removeAll { $0.transition is T }
}

/// Removes all registered `Transition` objects from the navigation controller.
public func unregisterAllTransitions() {
transitions.removeAll()
}
}

// MARK: - UINavigationControllerDelegate
Expand All @@ -78,17 +48,17 @@ extension NavigationController: UINavigationControllerDelegate {
from fromVC: UIViewController,
to toVC: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
let transitions = self.transitions.compactMap({ $0.transition })
guard
let provider = self.transitionProvider,
let from = (fromVC as? RouteProvider)?.route,
let to = (toVC as? RouteProvider)?.route
else {
return nil
}

for transition in transitions {
guard
let from = (fromVC as? RouteProvider)?.route,
let to = (toVC as? RouteProvider)?.route,
transition.isEligible(from: from,to: to, operation: operation)
else {
continue
}

if let transition = provider.transitions
.compactMap({ $0.transition })
.first(where: { $0.isEligible(from: from, to: to, operation: operation) }) {
return transition
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftUICoordinator/Transition/Transition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public typealias NavigationOperation = UINavigationController.Operation
///
/// - Returns: `true` if the transition is eligible; otherwise, `false`.
@MainActor
public protocol Transition: UIViewControllerAnimatedTransitioning {
public protocol Transitionable: UIViewControllerAnimatedTransitioning {
func isEligible(
from fromRoute: NavigationRoute,
to toRoute: NavigationRoute,
Expand Down
14 changes: 14 additions & 0 deletions Sources/SwiftUICoordinator/Transition/TransitionProvidable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// TransitionProvidable.swift
//
//
// Created by Erik Drobne on 12. 10. 23.
//

import Foundation

@MainActor
public protocol TransitionProvidable {
/// An array of transitions wrapped in weak references.
var transitions: [WeakTransition] { get }
}
20 changes: 20 additions & 0 deletions Sources/SwiftUICoordinator/Transition/TransitionProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// TransitionProvider.swift
//
//
// Created by Erik Drobne on 12. 10. 23.
//

import Foundation

@MainActor
public struct TransitionProvider: TransitionProvidable {
public private(set) var transitions: [WeakTransition]
/// A private array of transitions ensuring to retain them in memory as long as needed.
private var _transitions: [Transitionable]

public init(transitions: [Transitionable]) {
self._transitions = transitions
self.transitions = transitions.map { WeakTransition($0) }
}
}
4 changes: 2 additions & 2 deletions Sources/SwiftUICoordinator/Transition/WeakTransition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import Foundation
/// `WeakTransition` is used internally to store weak references to transitions.
public final class WeakTransition {
/// The weak reference to the transition.
public private(set) weak var transition: Transition?
public private(set) weak var transition: Transitionable?

/// Initializes a `WeakTransition` with the provided transition.
///
/// - Parameter transition: The transition to wrap with a weak reference.
init(_ transition: Transition) {
init(_ transition: Transitionable) {
self.transition = transition
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ class MockCoordinator: Routing {
self.startRoute = startRoute
}

func handle(_ action: CoordinatorAction) {

}
func handle(_ action: CoordinatorAction) {}
}

extension MockCoordinator: RouterViewFactory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import UIKit
import SwiftUICoordinator

class MockTransition: NSObject, Transition {
class MockTransition: NSObject, Transitionable {
func isEligible(from fromRoute: NavigationRoute, to toRoute: NavigationRoute, operation: NavigationOperation) -> Bool {
return (fromRoute as? MockRoute == .circle && toRoute as? MockRoute == .rectangle)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import XCTest
@MainActor
final class NavigationControllerTests: XCTestCase {

func test_NavigationBarIsHiddenByDefault() {
func test_navigationBarIsHiddenByDefault() {
let sut = NavigationController()
XCTAssertTrue(sut.isNavigationBarHidden)
}
Expand Down
6 changes: 2 additions & 4 deletions Tests/SwiftUICoordinatorTests/RootCoordinatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import XCTest
@MainActor
final class RootCoordinatorTests: XCTestCase {

func test_rootViewController() {
func test_rootViewControllerInitialization() {
let navigationController = NavigationController()
let window = UIWindow()
let sut = MockAppCoordinator(window: window, navigationController: navigationController)
Expand All @@ -20,8 +20,6 @@ final class RootCoordinatorTests: XCTestCase {
XCTAssertTrue(sut.window.isKeyWindow)
XCTAssertFalse(sut.window.isHidden)
XCTAssertEqual(sut.window.rootViewController, navigationController)
XCTAssertTrue(sut.childCoordinators.isEmpty)
}



}
30 changes: 30 additions & 0 deletions Tests/SwiftUICoordinatorTests/TransitionProviderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// TransitionProviderTests.swift
//
//
// Created by Erik Drobne on 23. 10. 23.
//

import XCTest
@testable import SwiftUICoordinator

@MainActor
class TransitionProviderTests: XCTestCase {

func test_transitionProviderInitialization() {
let transitions = [MockTransition(), MockTransition()]
let sut = TransitionProvider(transitions: transitions)

XCTAssertEqual(sut.transitions.count, transitions.count)
XCTAssertNotNil(sut.transitions.first?.transition)
}

func test_weakTransitionDoesNotRetain() {
var transition: Transitionable? = MockTransition()
let weakTransition: WeakTransition? = WeakTransition(transition!)

XCTAssertNotNil(weakTransition?.transition)
transition = nil
XCTAssertNil(weakTransition?.transition)
}
}
33 changes: 6 additions & 27 deletions Tests/SwiftUICoordinatorTests/TransitionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,16 @@ import Foundation
@MainActor
final class TransitionTests: XCTestCase {

func test_registerTransition() {
let coordinator = MockCoordinator(parent: nil, startRoute: .circle, navigationController: NavigationController())
let transitions = [MockTransition()]
func test_registerTransitions() {
let transition = MockTransition()
let provider = TransitionProvider(transitions: [transition])
let navigationController = NavigationController(transitionProvider: provider)

coordinator.navigationController.register(transitions)

guard let sut = coordinator.navigationController.transitions[0].transition as? MockTransition else {
guard let sut = navigationController.transitionProvider?.transitions[0].transition as? MockTransition else {
XCTFail("Cannot cast to MockTransition.")
return
}

XCTAssertEqual(sut, transitions[0])
}

func test_unregisterTransition() {
let sut = MockCoordinator(parent: nil, startRoute: .circle, navigationController: NavigationController())
let transitions = [MockTransition(), MockTransition()]
sut.navigationController.register(transitions)
XCTAssertEqual(sut.navigationController.transitions.count, 2)

sut.navigationController.unregister(MockTransition.self)
XCTAssertEqual(sut.navigationController.transitions.count, 0)
}

func test_unregisterAllTransitions() {
let sut = MockCoordinator(parent: nil, startRoute: .circle, navigationController: NavigationController())
let transitions = [MockTransition(), MockTransition(), MockTransition()]
sut.navigationController.register(transitions)
XCTAssertEqual(sut.navigationController.transitions.count, 3)

sut.navigationController.unregisterAllTransitions()
XCTAssertEqual(sut.navigationController.transitions.count, 0)
XCTAssertEqual(sut, transition)
}
}