iOS - Code Reviews
This article documents how to review code in One App iOS. Helpful for new and remote employees to get and stay aligned.
Philosophy
The code tells a story
Don’t make the reviewee cry
Value others time
Language: You review the code not the developer
Don’t put comments on the general PR if possible
Performing the code review
How to Review
General code smells
Guide (MVVM Architecture)
General module folders structure
Module dependencies
Scenes
Scene coordinator
Scene ViewController
Scene viewModel
Use cases
Guide (old architecture)
In general
Coordinators
Presenters
Other things to check in presenters
Use cases
Philosophy
Why are code reviews important?
Save your time - re-use coworker’s knowledge
Teach you new stuff - Learn from your coworkers
Show off your work - Share your knowledge with the team
The code tells a story
When you review a piece of code you should understand the functionality just reading it. If don’t, you usually have two ways of thinking about it:
I’m stupid, so probably this is ok, I will approve it. (that impostor syndrome again)
I don’t understand this code and it should be understood by a junior developer. It can be improved.
The second one must be the attitude to follow. Complex abstractions, overengineering, general and meaningless names, are a self-sabotage for
you and your workmates in the future.
Don’t make the reviewee cry
Reviewing is helping others, some times junior developers. Talk offline if it is necessary. Sometimes the problem is bad communication, poor
requirements or the scope of the problem.
If there is official documentation in Confluence about the review comment, add the link, this helps spreading the official conventions and good
practices.
Value others time
Most developers don’t review their own code, but coworkers aren’t your personal QA. You are responsible for your code quality. Review yourself
with the same criteria that other’s code.
Language: You review the code not the developer
Review the code, not the person. This must be considered in the language used. Don’t say “this is wrong”. Ask questions. Avoid negative
comments.
Examples:
You shoudn’t do it like this. Use optional
Bad review comment
This code would be better with optional. Here’s how to do it:...
Good review comment
Don’t put comments on the general PR if possible
Put comments in the diff in the appropiate point, not in the whole diff. When the comment is solved, be kind and write down an outcome (if you
have time). The comments are the history of its resolution and can be valuable in the future.
Performing the code review
How to Review
Make two passes over the PR if it's substantial.
On the first pass, come to an understanding of the code change at a high level, the modules affected paying attention to changes that can
affect several modules or other countries.
On the second pass, pay more attention to semantic details.
Review the main points listed in the Guide when it applies.
Check the structure when it is a new module
General code smells
A scene presentation module must not depend on another presentation module. the dependencies in a scene must be only vertical. For
example, the cards module must not depend on the login module.
Direct references to local country functionalities are not allowed. For example, add a library only for a country local functionality, adding
keys or naming related to a concrete One App vertical… in this cases core must offer a way of inject local country functionality.
Architecture mixtures. The conventions are clear about the separation of MVP and MVVM. Check the iOS - MVVM & MVP interoperability
articles to understand how both archs can work together.
A change in a public core API protocol. If you review a change in an existing Core public API protocol, this could break an app. Double
check that these kind of changes are protected with a default implementation, value or optionals, for example.
Guide (MVVM Architecture)
General module folders structure
In general it should use a template for the new module. There should not be a folder “standard” because it is from and old separation between
“standadard” and “legacy” modules that was deprecated.
It is correct to have an “MVVM” folder to group the module content.
Module dependencies
There will be a module/dependencies folder with two resolver files. One for local dependencies and one for external dependencies. See iOS -
Dependency injection (DI)
Things to check:
The folder exists
Exist a general dependency resolver file ModuleNameDependenciesResolver
ModuleNameDependenciesResolver protocol has a variable referencing the ExternalDependenciesResolver
var external: FundsHomeExternalDependenciesResolver { get }
Scenes
The module has scenes (see iOS - Scenes ) there is an standard structure that must be followed.
Scene coordinator
See the coordinator types in the architecture in the app navigation context, and general coordinator info
Things to check
A public protocol with the coordinator interface inheriting from BindableCoordinator
A default coordinator implementation. Naming starting with Default… and implementing the protocol
Usually we will have the following common implementation with an init and the lazy dependencies.
final class DefaultCoordinatorName:
CoordinatorProtocolName {
var onFinish: (() -> Void)?
weak var navigationController: UINavigationController?
var childCoordinators: [Coordinator] = []
private let externalDependencies:
SavingDetailExternalDependenciesResolver
lazy var dataBinding: DataBinding = dependencies.resolve()
private lazy var dependencies: Dependency = {
Dependency(dependencies: externalDependencies, coordinator:
self)
}()
public init(dependencies: ModuleDependenciesResolver,
navigationController: UINavigationController?) {
self.navigationController = navigationController
self.externalDependencies = dependencies
}
...
Scene ViewController
The view controller should be correctly related to the view model and the dependencies resolver.
Things to check:
private reference to the viewmodel protocol
i.e. private let viewModel: SavingDetailViewModelProtocol
private ref to dependencies resolver
i.e. private let dependencies: SavingDetailDependenciesResolver
A reference for the view model reactive subscriptions
private var anySubscriptions: Set<AnyCancellable> = []
an init
init(dependencies: SavingDetailDependenciesResolver) {
self.dependencies = dependencies
self.viewModel = dependencies.resolve()
super.init(nibName: nil, bundle: .module)
}
Scene viewModel
The view model uses a state mechanism (see iOS - ViewModel ) and at least a basic state enum and observable must be offered.
Things to check:
A state enum
Use cases
The use cases in MVVM architecture are described here in confluence.
Check list
Don’t import UIKit, UI or in general any device dependant API. Use cases should be only bussiness logic
Use cases returns a publisher with a representable. Never a DTO. (see iOS - Data Objects )
Guide (old architecture)
In general
In PLUI, adding the new controls to example app
Check that only custom views are added in PLUI. Complete reusable scenes must be added to PLScenes
Check the access level for classes, methods and properties.
Look for “import Commons”
Commons was moved to CoreFoundationLib in Feb 2022
Check // TODO: lines. We must avoid them. It generates warnings
Coordinators
A protocol to define the interface and its implementation in a extension
protocol PLBeforeLoginCoordinatorProtocol { }
...
extension PLBeforeLoginCoordinator : PLBeforeLoginCoordinatorProtocol {
...
}
A coordinator implementation of ModuleCoordinator. Note final modifier
final class PLBeforeLoginCoordinator: ModuleCoordinator {
weak var navigationController: UINavigationController?
private let dependenciesEngine: DependenciesResolver &
DependenciesInjector
init(dependenciesResolver: DependenciesResolver,
navigationController: UINavigationController?) {
self.navigationController = navigationController
self.dependenciesEngine = DependenciesDefault(father:
dependenciesResolver)
self.setupDependencies()
}
func start() {
...
}
}
Note
dependenciesEngine property is usually private. It make no sense a internal because it is the default access level.
A section with dependencies registration in a private extension
private extension PLBeforeLoginCoordinator {
func setupDependencies() {
...
}
}
Presenters
A presenter protocol to offer an interface and its implementation
protocol PLBeforeLoginPresenterProtocol: MenuTextWrapperProtocol {
...
}
...
extension PLBeforeLoginPresenter: PLBeforeLoginPresenterProtocol {
...
}
Usually with life cycle methods like viewDidLoad() and more specific methods.
The presenter implementation
final class PLBeforeLoginPresenter {
weak var view: PLBeforeLoginViewControllerProtocol?
init(dependenciesResolver: DependenciesResolver) {
self.dependenciesResolver = dependenciesResolver
}
}
Note:
final modifier
the dependencies resolver is inyected
the presenter has a weak reference to the scene view.
A private extension with more implementation
private extension PLBeforeLoginPresenter {
...
}
Other things to check in presenters
Use cases are launched from presenters using scenarios.
Use cases
The use case definition
final class PLGetPublicKeyUseCase: UseCase<Void,
PLGetPublicKeyUseCaseOkOutput, PLUseCaseErrorOutput<LoginErrorType>>,
PLLoginUseCaseErrorHandlerProtocol {
Note:
is marked final
Naming, ending in UseCase
The recommendation is defining a struct for Input and Output
Include dependenciesResolver in the init method
...
var dependenciesResolver: DependenciesResolver
public init(dependenciesResolver: DependenciesResolver) {
self.dependenciesResolver = dependenciesResolver
}
...
Implementation of executeUseCase method
public override func executeUseCase(requestValues: Void) throws ->
UseCaseResponse<PLGetPublicKeyUseCaseOkOutput,
PLUseCaseErrorOutput<LoginErrorType>> {
...
}
Other considerations
¿Is acceptable not using concrete input and output structs?
It depends, in some cases can be stupid using a struct to return a simple value.
It is an error returning a type defined in the data library.