So here is the challenge: You and your team are maintaining an iOS app that is growing out of control. Your classes are being called from everywhere and every change in your code is taking longer and longer. You need to do something.
Well, this happened to me a couple of times, and I've also worked on BIG projects where the team already had a solution implemented for this.
Let's have an overview on how the solution to this problem works in general.
You can split your app in several modules. The app will exist, of course, but functionality can be taken out of the main project.
So if you had a Dev.to-like app, you may have a module for post creation, another module for the main feed, etc. In order to generalize the concept, we'll call them
And how do we connect all of these small pieces into a single unit? We use dependency managers, such as Cocoapods (the one I used the most), or Carthage or Swift Package Manager.
By doing this, we can connect all the pieces together, bringing everything in the same
.xcworkspace (we'll use Cocoapods just as an example, but the same concepts apply to other package managers). In our example, each module will be a Pod, and they will integrate in our main app. So if we need to present the post creation view controller, we'll
import PostCreation and use the
PostCreationViewController class from that module. You got the idea.
There are some modules that could be called "Umbrella modules" or "Base modules". hey are modules that are used all across the project. A typical example is the networking module, the analytics module, etc. They are not tied to a single screen or part of the app, and they are also needed for the rest of the modules to work.
And how do we integrate each module with the others? I mean, imagine we need to call the Profile module from inside the Post Creation module, or things like that.
There is a simple though naïve solution, and a more complicated but better solution in my experience. Let's begin with the simple naïve solution: You can make a module depend on others. As far as there is no cyclic dependency (module A depends on module B, module B depends on module C and module C depends on module A), this will work. In this case we can have the Post Creation module depend on the Profile module by declaring the dependency in our
The problem with the naïve solution is that if you app is complex enough, fixed dependencies between different modules can become a huge compilation performance problem. In plain English, each module will wait for its dependencies to compile before compiling itself. And believe me, you don't want this. I've been waiting as long as an hour for my app to compile...
So, how do we fix this? There is a cleaner solution. By using a dependency injection framework, such as Swinject we can create a base module that we might call the DI module.
The DI module exposes a dependency injection container and protocols for all the cross-concern classes/structs/enums in the app. Modules can then "register" their implementations into the DI module, and other modules may then resolve their own dependencies. So, no module depends on another module directly, but just registers/resolves dependencies using the DI container.
Compilation speed will become much much better after doing this, because all the modules need to wait is the base modules to compile.
This is just scratching the surface but it's nice to know the why and how (at a high level) this is architected. Modularizing an app doesn't come without a cost and, as everything in software engineering, it's always a tradeoff. However, when you face an issue like this one, this is almost always a good idea. I've seen this pattern implemented many times in one or another way, and it's good to know this before working in a project this big.