Clean Swift (VIP) iOS Architecture Pattern
Quick overview
When implementing a Clean Swift project your code will be structured around each of your application screens or segments of screens, also known as “scenes”.
In theory, each scene is a structure with around 6 components:
-
View Controller,
-
Interactor,
-
Presenter,
-
Worker,
-
Models,
-
Router.
The view controller, interactor, and presenter are the three main components of Clean Swift. They act as input and output to one another as shown in the following diagram.
Example
Imagine a screen with a login button. It's a Scene that defines a structure with a VIP cycle of View Controller, Interactor, and Presenter. When the user taps the button, the View Controller calls the interactor. The interactor uses the business logic inside to prepare an output (with the use of workers). It then propagates the result to the presenter. The presenter calls the VC’s method to call the router to display a new scene.
View Controller
-
Defines a scene and contains a view or views.
-
Keeps instances of the interactor and router.
-
Passes the actions from views to the interactor (output) and takes the presenter actions as input.
protocol LoginSceneViewControllerInput: AnyObject {
func showLogingSuccess(fullUserName: String)
func showLogingFailure(message: String)
}
protocol LoginSceneViewControllerOutput: AnyObject {
func tryToLogIn()
}
final class LoginSceneViewController: UIViewController {
var interactor: LoginSceneInteractorInput?
var router: LoginSceneRoutingLogic?
private var loginButton: UIButton = {...}()
}
private extension LoginSceneViewController {
@objc func loginButtonAction() {
interactor?.tryToLogIn()
}
}
extension LoginSceneViewController: LoginSceneViewControllerInput {
func showLogingSuccess(fullUserName: String) {
router?.showLoginSuccess()
}
func showLogingFailure(message: String) {
router?.showLogingFailure(message: message)
}
}
Interactor
-
Contains a Scene’s business logic.
-
Keeps a reference to the presenter.
-
Runs actions on workers based on input (from the View Controller), triggers and passes the output to the presenter.
-
The interactor should never import the UIKit.
typealias LoginSceneInteractorInput = LoginSceneViewControllerOutput
protocol LoginInteractorOutput: AnyObject {
func showLogingSuccess(user: CleanLoginUser)
func showLogingFailure(message: String)
}
final class LoginSceneInteractor {
var presenter: LoginScenePresenterInput?
var authWorker: LoginSceneAuthLogic?
}
extension LoginSceneInteractor: LoginSceneInteractorInput {
func tryToLogIn() {
authWorker?.makeAuth(completion: { result in
DispatchQueue.main.async { [weak self] in
switch result {
case .success(let data):
self?.presenter?.showLogingSuccess(user: data)
case .failure(let error):
self?.presenter?.showLogingFailure(message: error.localizedDescription)
}
}
})
}
}
Worker
-
An abstraction that handles different under-the-hood operations like fetch the user from Core Data, download the profile photo, allows users to like and follow, etc.
-
Should follow the Single Responsibility principle (an interactor may contain many workers with different responsibilities).
protocol LoginSceneAuthLogic {
func makeAuth(
completion: @escaping (Result<CleanLoginUser, LoginSceneAuthWorker.LoginSceneAuthWorkerError>
) -> Void)
}
final class LoginSceneAuthWorker {
private let service: AuthService
private var bag = Set<AnyCancellable>()
init(service: AuthService) {
self.service = service
}
enum LoginSceneAuthWorkerError: Error {
case authFailed(String)
case unauthorized
}
}
extension LoginSceneAuthWorker: LoginSceneAuthLogic {
func makeAuth(
completion: @escaping (Result<CleanLoginUser, LoginSceneAuthWorkerError>
) -> Void) {
service.auth()
.sink { _ in } receiveValue: { value in
switch value.authorized {
case true:
completion(.success(CleanLoginUser()))
case false:
completion(.failure(.unauthorized))
}
}
.store(in: &bag)
}
}
Presenter
-
Keeps a weak reference to the view controller that is an output of the presenter.
-
After the interactor produces some results, it passes the response to the presenter. Next, the presenter marshals the response into view models suitable for display and then passes the view models back to the view controller for display to the user.
typealias LoginScenePresenterInput = LoginInteractorOutput
typealias LoginScenePresenterOutput = LoginSceneViewControllerInput
final class LoginScenePresenter {
weak var viewController: LoginScenePresenterOutput?
}
extension LoginScenePresenter: LoginScenePresenterInput {
func showLogingFailure(message: String) {
viewController?.showLogingFailure(message: "")
}
func showLogingSuccess(user: CleanLoginUser) {
viewController?.showLogingSuccess(fullUserName: user.firstName + " " + user.lastName)
}
}
Router
-
Extracts this navigation logic out of the view controller.
-
Keeps a weak reference to the source (View Controller).
protocol LoginSceneRoutingLogic {
func showLoginSuccess()
func showLogingFailure(message: String)
}
final class LoginSceneRouter {
weak var source: UIViewController?
private let sceneFactory: SceneFactory
init(sceneFactory: SceneFactory) {
self.sceneFactory = sceneFactory
}
}
extension LoginSceneRouter: LoginSceneRoutingLogic {
func showLogingFailure(message: String) {
source?.present(UIAlertController.failure(message), animated: true)
}
func showLoginSuccess() {
let scene = sceneFactory.makeLoginScene()
source?.navigationController?.pushViewController(scene, animated: true)
}
}
Configurator
- Takes the responsibility of configuring the VIP cycle by encapsulating the creation of all instances and assigning them where needed.
protocol LoginSceneConfigurator {
func configured(_ vc: LoginSceneViewController) -> LoginSceneViewController
}
final class DefaultLoginSceneConfigurator: LoginSceneConfigurator {
private var sceneFactory: SceneFactory
init(sceneFactory: SceneFactory) {
self.sceneFactory = sceneFactory
}
@discardableResult
func configured(_ vc: LoginSceneViewController) -> LoginSceneViewController {
sceneFactory.configurator = self
let service = DefaultAuthService(
networkManager: DefaultNetworkManager(session: MockNetworkSession())
)
let authWorker = LoginSceneAuthWorker(service: service)
let interactor = LoginSceneInteractor()
let presenter = LoginScenePresenter()
let router = LoginSceneRouter(sceneFactory: sceneFactory)
router.source = vc
presenter.viewController = vc
interactor.presenter = presenter
interactor.authWorker = authWorker
vc.interactor = interactor
vc.router = router
return vc
}
}
Model
-
Decoupled data abstractions.
Full schema
Other
To avoid memory leaks always pass the view controller to the router and presenter as a weak reference.
I’m not the biggest fan of the clean-swift.com. I encourage you to check the sample project of CleanStore. Or just follow the examples above (examples are from my sample project on GitHub).
Key Rules
-
Keep the file structure (follow the Scene naming).
-
Use VIP cycle and input/output protocols:
-
The view controller accepts a user event, constructs a request object, and sends it to the interactor.
-
The interactor does some work with the request, constructs a response object, and sends it to the presenter.
-
The presenter formats the data in the response, constructs a view model object, and sends it to the view controller.
-
The view controller displays the results contained in the view model to the user.
-
Recommendations
-
Projects where unit testing is expected.
-
Long-term and big projects.
-
Projects with a generous amount of logic.
-
Projects you want to reuse in the future.
-
When MVVM, MVP, MVC are not enough or you just hate VIPER but there is a need to introduce a sophisticated architecture.
-
Native and imperative projects.
Strengths
-
Easy to maintain and fix bugs.
-
Enforces modularity to write shorter methods with a single responsibility.
-
Nice for decoupling class dependencies with established boundaries.
-
Extracts business logic from view controllers into interactors.
-
Nice to build reusable components with workers and service objects.
-
Encourages to write factored code from the start with clean-swift.com
-
Applies to existing projects of any size.
-
Modular: Interfaces may be easy to change without changing the rest of the system due to using protocol conformant business logic
-
Independent from the database.
Disadvantages and traps
-
Many protocols with complicated naming and responsibilities, at first it may be confusing where the protocol is defined.
-
Large app size due to many protocols.
-
Despite the fact that there is an official website of Clean Swift architecture, it changes often and implementations may differ between projects.
-
It’s hard to maintain the separation between VC and presenter. Sometimes the presenter just calls the view methods instead of preparing the UI, so it seems useless and just creates boilerplate.
Learning materials
Summary
Clean Swift is not the easiest to maintain, but possibly the best to avoid coupling and be able to keep a high code coverage score.
The VIP cycle enforces a strict naming convention and class responsibilities with a medium amount of boilerplate code.
Would you like to see a simple example of Clean Swift architecture?
Check my sample project ‘CleanLogin’ on GitHub.