reference
- Custom container view controllers in Swift
- Logic controllers in Swift
- Model controllers in Swift
- Refactoring Swift code for testability
- Self-manager mode in iOS development
The purpose of this article is to summarize some ways that code quality and maintainability can be improved over time without major changes to the existing project structure.
The guiding ideology
Clearly distinguish the various functions and systems of your application according to their responsibilities and concerns.
purpose
The ultimate goal of all the architectural patterns, techniques, and principles we invent is to lead us to write cleaner, decoupled, and maintainable code.
Container view controller
The problem with the proliferation of view controllers is that they have too many responsibilities: managing views, performing layouts and handling events, as well as managing networking, image loading, caching and many other things.
One way to reduce growth is to split them up, with each view controller responsible for a smaller area of responsibility, which is then controlled by a container view controller.
- Content view controllers can focus on layout and rendering specific states
- Container view controllers are responsible for adding and removing content view controllers, as well as transitions between various content states
- The other nice thing about this is that UIKit takes care of automatically sending all the standard UIViewController events to the child view controllers
By nesting view controllers, you usually end up with a more modular, simpler implementation.
Logic controller
In general, there are two approaches to splitting large types into parts – composition and extraction.
Using container view controllers above is a composite approach, but that alone won’t solve all the problems, and in some cases it can add a lot of complexity with little benefit. In this case, it may be better to extract functionality into a separate, specialized type.
Extraction is the process of pulling out large parts into separate types that are tightly bound to the original. This approach is common in various architectural patterns – such as MVVM, which introduces the View Model type to handle most of the Model-> View transformation logic.
Another easy way to maintain the MVC pattern is to split the view controller into the view part and the controller part:
- A controller preserves the UIViewController subclass and all view-related functionality
- The other controller is separate from the UI and focuses on handling the logic
- The view controller doesn’t need to know how its state is loaded, just use the state provided by the logical controller and render it, right
The Self – Manager mode
Give a View more power and make it responsible for all its own events and logic.
For external consumers, it is simply a matter of passing the necessary dependencies without any concern or involvement in any logic.
For complex view controllers, the ability to extract some modules for self-management and even reuse can obviously make the corresponding logic clearer and simpler.
Model controller
Most applications contain many different types of models, which can generally be divided into two categories – shared and local.
The shared model is used in many different modules, and there is usually some shared logic.
Putting this logic in some form of singleton or global function would generally hurt our code encapsulation, but it would be strange to put it directly into a model, because one thing most design patterns agree on is that ideally a model should be a simple data container with little logic.
This is the time to introduce a model controller that executes all the logic associated with a single instance of the model – let’s properly encapsulate model-specific logic:
- Further control and hide sensitive information to prevent external misuse and modification
- A single dedicated code path can be fully unit-tested, eliminating the risk of repetitive logic and inconsistencies
Rational dependency injection
Dependency injection is an essential tool when it comes to making code easier to test.
Rather than having objects create their own dependencies or access them as singletons, it is assumed that everything an object needs should be passed externally.
This not only makes it easier to see what exact dependencies a given object has, but it also makes testing simpler – because you can emulate dependencies to capture and validate state and value.
Especially when singletons are used directly inside objects, it is not only difficult to test, but also may produce an ambiguous relationship between the two.