I continue the series of articles about the library RouteComposer we use, and today I want to talk about the Coordinator pattern. By writing this article, I was inspired by the discussion of one of the articles on the Coordinator pattern here on Habré.
The Pattern Coordinator, having been introduced not so long ago, is gaining more and more popularity in IOS developer circles, and, in general, it is clear why. Because the tools out of the box that UIKit provides are a rather non-universal mess.
I have already raised the issue of fragmentation of ways of composition with the view controllers on the stack, and to avoid repetition - you can just read about it here .
Let's be honest. At some point, and Epol realized that putting twist controllers in the application development center, she did not offer any sensible way to create or transfer data between them, and, having ordered the solution of this problem to the developers of autocompilers from Xcode, but can the developers of UISearchConnroller, at some point presented storyboards and segues. Then Epol realized that the application consisting of 2 screens she writes only herself, and in the next iteration offered the opportunity to split storyboards into several parts, as Xcode began to crash when it reached a certain size. Segues have changed with this concept, in several iterations that are not very compatible with each other. Their support is tightly embedded in the massive UIViewController
class, and, ultimately, we got what we got. This:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "showDetail" { if let indexPath = tableView.indexPathForSelectedRow { let object = objects[indexPath.row] as! NSDate let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController controller.detailItem = object controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem controller.navigationItem.leftItemsSupplementBackButton = true } } }
The number of force typkasts in this block of code is amazing, as are the string constants in the storyboards themselves, for which Xcode does not offer any means to track. And the slightest desire to change something in the process of navigation will allow you to compile the project without any effort, and it will crash into runtime with a cheerful crackling sound without any warning from Xcode. Here is such a WYSIWYG eventually turned out. What you see is what you get.
One can argue about the charms of these gray arrows in the storyboards supposedly showing someone the connections between the screens, but, as my practice showed, and I deliberately interviewed several familiar developers from different companies, as soon as the project grew beyond 5-6 screens, people tried find a more reliable solution and finally began to keep the structure of the stack of view controllers in my head. And if you added support for the iPad and another navigation model, or support for pushing, then everything was generally sad.
Since then, several attempts have been made to solve this problem, some of which turned into separate frameworks, some into separate architectural patterns, because by creating the view controller inside the controller view, this massive and unwieldy piece of code made even more.
Let's return to the Coordinator pattern. For obvious reasons, you will not find its description in Wikipedia because it is not a standard programming / design pattern. Rather, it is a kind of abstraction that suggests hiding under the hood all this “ugly” code of creating and inserting into the stack a new view of the controller, storing references to the container controllers and pushing data between the controllers. The most useful article describing this process, I would call an article on raywenderlich.com . It is starting to become popular after the 2015 NSSpain conference, when it was told to the general public. In more detail what has been told can be found here and here .
I will briefly describe what it is before moving on.
In all interpretations the Pattern Coordinator fits approximately into this picture:
That is, the coordinator is a protocol
protocol Coordinator { func start() }
And all the “ugly” code is supposed to be hidden in the start
function. The coordinator, in addition, may have links to child coordinators, that is, they have some ability to compose, and, for example, you can replace one implementation with another. That is, it sounds pretty elegant.
However, non-elegance starts pretty soon:
UINavigationController
, to handle pressing the Back button or swipe back and remove the child coordinator. For natural reasons, only one object can be a delegate, which limits the ability to control the container itself and leads to the fact that this logic either lies with the coordinator or creates the need to delegate this logic further to someone else on the list.UITabBarController
controllers, such as UINavigationController
, UITabBarController
and so on. And someone has to provide links to these controllers to them. If everything is more or less clear with the child coordinators, then the initial coordinators of the chain are not so simple. Plus, when changing the navigation, for example for the A / B test, refactoring and adaptation of such coordinators results in a separate headache. Especially if the type of container changes.if/else
begin to spread over the coordinators or go to another Mega-Coordinator in the form of a piece of spaghetti. Plus, there are either active iterations of the stack of view controllers and an attempt to determine where the user is at the moment, or an attempt to build an application that monitors its state, but this is not a very simple task, simply based on the nature of the stack of controllers.UITabBarController
, which has a UINavigationController
in the second tab with some other UIViewController
. The user in the first tab causes some kind of event that requires switching the tab and UINavigationController
another controller in his UINavigationController
. This is all you need to do in that order. If the user has never opened the second tab before this, and the UINavigationController
not called the viewDidLoad
method, push
will not work, leaving only a vague message in the console. That is, the coordinators cannot simply be made listeners of the events in this example; they must work in a certain sequence. So must have knowledge of each other. And this already contradicts the first statement of the Coordinator pattern, that the coordinators do not know anything about the generating coordinators and are connected only with the child ones. And also limits their interchangeability.This list can be continued, but in general it is clear that the Coordinator pattern is a rather limited, poorly scalable solution. If you look at it without rose-colored glasses, then it is a way of decomposing a part of logic, which is usually written inside the massive UIViewController
, into another class. All attempts to make it something more than a kind of generating factory and introduce there another logic, do not end too well.
It is worth noting that there are libraries based on this pattern, which with one success or another allow to partially offset the listed disadvantages. I would point out XCoordinator and RxFlow .
Having played in the project that we got from the other team for support and development, with the coordinators and their simplified “great-grandmother” Routers in the VIPER architectural approach, we rolled back to an approach that proved itself well in the previous large project of our company. There is no name for this approach. It lies on the surface. When we had free time, it was isolated in a separate library RouteComposer, which completely replaced the coordinators and showed itself more flexible.
What is this approach? In order to rely on the stack (tree) I twist the controllers as it is. That would not create extra entities that need to be monitored. Do not save or track states.
Let's take a closer look at the UIKit entities and try to figure out what we have in the bottom line and what you can work with:
UINavigationController
, UITabBarController
and so on, but can be created by the user. If you abstract, you can find the following properties in all containers: 1. They have a list of all controllers that they contain. 2. One or more controllers are currently visible. 3. They may be asked to make one of these controllers visible. This is all that I twist UIKit controllers. They just have different methods for this. But the tasks are only 3.UINavigationController.pushViewController(...)
, UITabBarController.selectedViewController = ...
, UIViewController.present(...)
and so on. You may notice that you always need 2 views of the controller, one already on the stack, and one that needs to be built into the stack. Wrap it in a wrapper and call it an Action . Each action is easy to cover with exhaustive unit tests and each is independent of the others.An example of this configuration:
let productScreen = StepAssembly(finder: ProductViewControllerFinder(), factory: ProductViewControllerFactory()) .add(LoginInterceptor<UUID>()) // Have to specify the context type till https://bugs.swift.org/browse/SR-8719 is fixed .add(ProductViewControllerContextTask()) .add(ProductViewControllerPostTask(analyticsManager: AnalyticsManager.sharedInstance)) .using(UINavigationController.push()) .from(NavigationControllerStep()) .using(GeneralActions.presentModally()) .from(GeneralStep.current()) .assemble()
The items described above cover the entire library and describe the approach. All that remains is for us to provide configurations of the chains that the router will perform when the user presses the button or an external event occurs. If these are different types of devices, for example, iPhone or iPad, then we will provide different transition configurations using polymorphism. If we have A / B testing, that's the same thing. We do not need to think about the state of the application at the time of the start of navigation, we need to make sure that the configuration was written correctly initially, and we are sure that the router will build it one way or another.
The described approach is more complicated than a certain abstraction or pattern, but we have not yet encountered a task where it would not be enough. Of course, RouteComposer requires some study and understanding of how it works. However, this is much easier than learning the basics of AutoLayout or RunLoop. No higher mathematics.
The library, as well as the implementation of the router provided to it, does not use any tricks with objective runtime and fully follows all the concepts of Cocoa Touch, only helping to break up the composition process into steps and performs them in a predetermined sequence. Library tested with iOS versions 9 through 12.
More details can be found in previous articles:
Composition of UIViewControllers and navigation between them (and not only) / Habr
Configuration examples of UIViewControllers using RouteComposer / Habr
Thanks for attention. I am pleased to answer questions in the comments.
Source: https://habr.com/ru/post/446550/
All Articles