Group Group Group Group Group Group Group Group Group

Setting view controllers frame in PickMeUpViewController & Coordinators

#1

First of all, I wanted to say the book is awesome and has really filled the knowledge gap for me, especially the Dependency Injection chapter. Fantastic work, best book so far I’ve read on iOS.

As for the questions:

1. Chapter 5

In PickMeUpViewController, in method

presentRideOptionPicker

the child view’s frame is set, I’m wondering why was it not set using constraints? I’m curious because I haven’t been using this method of too much.

2. Chapter 4 - Following the MVVM-Coordinator architecture, I’m wondering what would be the best place to create the first dependency container, and I’m thinking the coordinators should have a constructor dependency of the container they should need dependencies of - is that the correct thinking? Let’s say there’s a main AppCoordinator, and then SignedInCoordinator and UnsignedCoordinator, and AppCoordinator would require AppDependencyContainer, and the child coordinator would require their own dependency containers. There’s no point in creating the containers inside the coordinators, right? That would create access to unwanted methods/variables in them.

Is using Coordinators a good idea now?

Best regards,
Alex

#2

@jab2109 @rcach Can you please help with this when you get a chance? Thank you - much appreciated! :]

#3

@thaxsillion

Hi Alex, thanks for your kind words. I’m really glad that you’ve found the book helpful. Here’s a crack at answering the questions.

1.
We aren’t doing anything special here, we just haven’t gotten a chance to make sure all of our layouts are responsive by using constraints. Our focus on the code, so far, has been on getting the architecture right and so some things like view layout need some more refinement. Having said that, sometimes the layout math is so simple that it might make more sense to layout views manually by setting frames. This is how we used to implement view layout before we had constraints, way back in the day. The trick is in updating frames in viewDidLayoutSubviews(). But in general I prefer to use layout constraints.

2.
Generally, the best place to create your app’s root dependency container is in the app delegate. This is because the container manages your app’s object graph, starting with the window’s root view controller. Otherwise, if you place the container in a view controller, your app delegate cannot use Dependency Injection to create the window’s root view controller.

When using the coordinator pattern, coordinators are just like any other object created by a dependency container. To make this work, a dependency container would have a factory method for creating a coordinator. For example, coordinators need a reference to the view controller they are managing. The coordinator depends on the view controller. So the dependency container can create the view controller first and then create the coordinator with the view controller. I would have the AppDependencyContainer know how to instantiate an AppCoordinator. The container would most likely hold on to the app coordinator since the app coordinator probably needs to live for the life of the app’s process, i.e. app scope.

Regarding scoped child dependency containers with coordinators, you have a couple of options. The goal is to not allow parent dependency containers to access child dependency containers. This is because the child comes and goes and so there’s a chance the child container won’t be there when resolving a dependency. But something has to hold onto child dependency containers, otherwise ARC will deallocate them. In the example code, we don’t have to worry about this because the SignedInViewController happens to capture a reference to the signed-in dependency container via the view controller factory property. If a scoped view controller doesn’t happen to hold a reference, there are a couple of options:

  1. Add a property to the AppDependencyContainer type annotated with AnyObject for the child signed-in dependency container. Typing it as AnyObject helps remind anyone working in the file that the parent should not call into any child dependency containers and actually prevents direct calls. The challenge with this approach is that the app has to signal to the AppDependencyContainer when the user has exited the scope, in this case the signed-in scope. That way, the AppDependencyContainer can nil out the property and ensure the signed-in dependency container is deallocated.
  2. Alternatively, you can inject your AppCoordinator with a couple of factory closures. One closure would be capable of instantiating an UnsignedDependencyContainer and the other would be capable of instantiating a SignedInDependencyContainer. When the user switches from not-signed-in to signed-in, the AppCoordinator would call the signed-in dependency factory closure to create a new scoped dependency container for the signed-in scope. The AppCoordinator can then hold onto this dependency container with a property. Then, the AppCoordinator can use the SignedInDependencyContainer to create the SignedInCoordinator. What’s nice about this approach is that the AppCoordinator knows when the user is moving from one scope to the other, so it is in the best position to manage when the child dependency containers need to be created and destroyed. This also keeps the child dependency containers away from the parent container. Some people don’t like this because Dependency Injection makes its way into application objects. I personally don’t mind this because we are working at the highest levels of the app and we are still keeping Dependency Injection out of content view controllers and their views.

Here’s some code that demonstrates each point:

1. Holding onto children in parent containers:

class AppDependencyContainer {
  var unsignedInDependencyContainer: AnyObject?
  var signedInDependencyContainer: AnyObject?

  // Other app scoped objects
  ...

  func makeUnsignedInCoordinator() -> UnsignedInCoordinator {
    let unsignedInDependencyContainer = makeUnsignedInDependencyContainer()
    // Ask the scoped container to create fully injected unsigned coordinator (including the coordinator's vc)
    let coordinator = unsignedInDependencyContainer.makeUnsignedInCoordinator()

    return coordinator
  }
 
  func makeUnsignedInDependencyContainer() -> UnsignedInDependencyContainer {
    // Deallocate other child container if present 
    self.signedInDependencyContainer = nil
    // Create new scoped container
    let _unsignedInDependencyContainer = UnsignedInDependencyContainer(parent: self)
    // Hold onto it
    self.unsignedInDependencyContainer = _unsignedInDependencyContainer

    return _unsignedInDependencyContainer
  } 

  func makeSignedInCoordinator() -> SignedInCoordinator {
    let signedInDependencyContainer = makeSignedInDependencyContainer()
    // Ask the scoped container to create fully injected unsigned coordinator (including the coordinator's vc)
    let coordinator = signedInDependencyContainer.makeSignedInCoordinator()

    return coordinator
  }

  func makeSignedInDependencyContainer() -> SignedInDependencyContainer {
    // Deallocate other child container if present 
    self.unsignedInDependencyContainer = nil
    // Create new scoped container
    let _signedInDependencyContainer = SignedInDependencyContainer(parent: self)
    // Hold onto it
    self.signedInDependencyContainer = _signedInDependencyContainer

    return _signedInDependencyContainer
  }

  // Other factory methods
  ...
}

2. Holding onto child containers in coordinators

class AppCoordinator {
  // Coordinator's view controller
  let rootViewController: MainViewController

  // Dependency container factories, provided by root `AppDependencyContainer`
  let makeUnsignedInDependencyContainer: () -> UnsignedInDependencyContainer
  let makeSignedInDependencyContainer: (User) -> SignedInDependencyContainer

  // Properties to keep scoped dependency containers alive
  var unsignedInDependencyContainer: AnyObject?
  var signedInDependencyContainer: AnyObject?

  // Child coordinators, only needed if app coordinator needs to call into these after presentation
  var unsignedInCoordinator: UnsignedInCoordinator?
  var signedInCoordinator: SignedInCoordinator?

  // `AppDependencyContainer` creates this coordinator with VC and DI container factory closures
  init(rootViewController: MainViewController,
         makeUnsignedInDependencyContainer: () -> UnsignedInDependencyContainer,
         makeSignedInDependencyContainer: (User) -> SignedInDependencyContainer) {
    self. rootViewController = rootViewController
    self.makeUnsignedInDependencyContainer = makeUnsignedInDependencyContainer
    self. signedInDependencyContainer = signedInDependencyContainer
  }

  func presentUnsignedIn() {
    // Clear out / deallocate signed-in scope if present
   self.signedInDependencyContainer = nil
   self.signedInCoordinator = nil

    // Create new scoped container and use it to create child coordinator
    let _unsignedInDependencyContainer = makeUnsignedInDependencyContainer()
    let _ unsignedInCoordinator = _unsignedInDependencyContainer.makeUnsignedInCoordinator()
    
    // Somehow present unsignedInCoordinator's view controller...
   ...

   // Hold onto new container and child coordinator
   self.unsignedInDependencyContainer = _unsignedInDependencyContainer
   self.unsignedInCoordinator = _unsignedInCoordinator
  }

  func presentSignedIn(user: User) {
    // Clear out / deallocate signed-in scope if present
   self.unsignedInDependencyContainer = nil
   self.unsignedInCoordinator = nil

    // Create new scoped container and use it to create child coordinator
    let _signedInDependencyContainer = makeSignedInDependencyContainer(user)
    let _ signedInCoordinator = _signedInDependencyContainer.makeSignedInCoordinator()
    
    // Somehow present signedInCoordinator's view controller...
   ...

   // Hold onto new container and child coordinator
   self.signedInDependencyContainer = _signedInDependencyContainer
   self.signedInCoordinator = _signedInCoordinator
  }
}

Having said all that, I think folks don’t really need coordinators in iOS. Instead of coordinators, you can implement custom container view controllers. They serve the same purpose, manage navigation among child view controllers. This works even for nav controllers, you can subclass UINavigationController if needed. That’s what we do in the example code. The MainViewController's only responsibility is to manage the app’s top level navigation. I like this approach because using container view controllers does not require tying yet another kind of object to view controllers. You can take the same approaches outlined above, the only difference is that instead of having a coordinator + view controller, you just have a container view controller that holds onto the child dependency containers and manages when the user moves from one scope to another.

If anything doesn’t make sense feel free to send follow up questions. Hope this helps! Happy architecting.

1 Like