How can I have 2 generics use the same T type

Viewed 74

Imagine I have a view controller factory with the following type:

protocol ViewFactoryType {
  associatedtype T
  func create(for scene: T) -> UIViewController
}

final class ViewFactory<T>: ViewFactoryType {
  func create(for scene: T) -> UIViewController {
    return .init()
  }
}

And I also have an navigation router with the following type:

protocol RouterType {
  associatedtype T
  func navigate(to target: T, using transition: Transition)
}

What I would like to do is create an instance of RouterType and pass it and instance of a ViewFactory.

Both generic T types will in fact use the same Scene type.

I have tried to create that router as follows:

final class AppRouter<T>: RouterType {

  private let viewControllerFactory: ViewFactoryType
  private let navigationController: UINavigationController

  public init(navigationController: UINavigationController, viewControllerFactory: ViewFactoryType) {
    self.navigationController = navigationController
    self.viewControllerFactory = viewControllerFactory
  }

  public func navigate(to target: T, using transition: Transition) {
    routeTo(viewControllerFactory.create(for: target), transition: transition)
  }

  private func routeTo(_ viewController: UIViewController, transition: Transition) {
    switch transition {
    case .push: navigationController.pushViewController(viewController, animated: true)
    case .present: navigationController.present(viewController, animated: true)
    case .replace: navigationController.setViewControllers([viewController], animated: false)
    }
  }
}

However this produces the error:

Protocol 'ViewFactoryType' can only be used as a generic constraint because it has Self or associated type requirements

and

Member 'create' cannot be used on value of protocol type 'ViewFactoryType'; use a generic constraint instead

I was able to get around this slightly by using an extension:

final class AppRouter<T> {

  private let viewControllerFactory: ViewFactoryType
  private let navigationController: UINavigationController

  public init(navigationController: UINavigationController, viewControllerFactory: ViewFactoryType) {
    self.navigationController = navigationController
    self.viewControllerFactory = viewControllerFactory
  }

  private func routeTo(_ viewController: UIViewController, transition: Transition) {
    switch transition {
    case .push: navigationController.pushViewController(viewController, animated: true)
    case .present: navigationController.present(viewController, animated: true)
    case .replace: navigationController.setViewControllers([viewController], animated: false)
    }
  }
}

extension AppRouter: Router where T == Scene {
  func route(to target: Scene, as transition: Transition) {

  }
}

But the factory method does not have the correct type if I add it too func route(to target: Scene, as transition: Transition)

1 Answers

You can't pass the associatedtype properties in that fashion. The compiler does not understand how they should relate or how you expect them to be used.

You could refactor your AppRouter to accept a type that represents the ViewFactoryType it should expect.

final class AppRouter<T, R> {

  private let viewControllerFactory: R
  private let navigationController: UINavigationController

  public init(navigationController: UINavigationController, viewControllerFactory: R) {
    self.navigationController = navigationController
    self.viewControllerFactory = viewControllerFactory
  }

  private func routeTo(_ viewController: UIViewController, transition: Transition) {
    switch transition {
    case .push: navigationController.pushViewController(viewController, animated: true)
    case .present: navigationController.present(viewController, animated: true)
    case .replace: navigationController.setViewControllers([viewController], animated: false)
    }
  }
}

extension AppRouter: RouterType where T == Scene, R == ViewFactory<T> {
  func navigate(to target: Scene, as transition: Transition) {
    let viewController = viewControllerFactory.create(for: target)
    routeTo(viewController, transition: transition)
  }
}

You can create an instance by passing the correct type now

    let viewControllerFactory = ViewFactory<Scene>()
    let router = AppRouter<Scene, ViewFactory<Scene>>(navigationController: navigationController, viewControllerFactory: viewControllerFactory)
Related