The Call of the Open Sea π
Ahoy there, fellow developers and tech explorers! Gather 'round as we embark on a swashbuckling adventure, navigating the treacherous waters of iOS app development. Picture this: a challenging deadline of just three months to complete our grand project. With the odds stacked against us, we unfurled the VIP pattern flagβan architecture that promised both elegance and cleanlinessβto set sail on this thrilling journey. But, wait! As we boarded our VIP ship, we added three daring new crew members to aid us in navigating these perilous seas: Captain Coordinator, the Clever Monkey, and the Versatile Controller!
The Perfect Storm: Facing the Three-Month Deadline πͺοΈ
As the project loomed before us like a tempest, our iOS team knew we needed an architecture that would guide us through these stormy waters. VIP, or "View-Interactor-Presenter," seemed like the perfect choice, offering a clean and modular structure to ease our daily development struggles. It was time to raise the VIP flag and chart our course towards victory!
The old VIP way before our magic π΄π» :
VIP stands for "View, Interactor, Presenter"
Here's a high-level explanation of each component in the VIP pattern:
View (View Controller): Responsible for displaying the user interface and handling user interactions. It forwards the user input to the Interactor and receives data from the Presenter to update the UI.
Interactor: Contains the business logic and data management of the application. It communicates with the Presenter to deliver the processed data.
Presenter: Acts as a mediator between the View and the Interactor. It receives data from the Interactor and formats it for presentation in the View. The Presenter also handles user interactions from the View and communicates with the Interactor to perform relevant actions.
The view
import UIKit
protocol MyViewProtocol: class {
func displayData(_ data: String)
}
class MyViewController: UIViewController, MyViewProtocol {
var presenter: MyPresenterProtocol!
// Your UI components and outlets
override func viewDidLoad() {
super.viewDidLoad()
presenter.viewDidLoad()
}
// Implement the displayData method to update the UI
func displayData(_ data: String) {
// Update your UI elements with the data received from the Presenter
// Example: myLabel.text = data
}
// Handle user interactions and forward them to the Presenter
// Example: when a button is tapped
@IBAction func buttonTapped(_ sender: UIButton) {
presenter.didTapButton()
}
}
The Interactor
protocol MyInteractorProtocol: class {
func fetchData()
}
class MyInteractor: MyInteractorProtocol {
weak var presenter: MyPresenterProtocol?
// Implement the fetchData method to retrieve data from a data source
func fetchData() {
// Perform data fetching and processing
let data = "Data fetched from the server"
presenter?.dataFetched(data)
}
}
The Presenter
protocol MyPresenterProtocol: class {
func viewDidLoad()
func didTapButton()
func dataFetched(_ data: String)
}
class MyPresenter: MyPresenterProtocol {
weak var view: MyViewProtocol?
var interactor: MyInteractorProtocol!
// Called when the View is loaded
func viewDidLoad() {
interactor.fetchData()
}
// Called when the button is tapped in the View
func didTapButton() {
// Perform actions or additional processing
}
// Called when data is fetched from the Interactor
func dataFetched(_ data: String) {
view?.displayData(data)
}
}
Introducing our own Fearless VIP Crew: Captain Coordinator, Clever Monkey, and Versatile Controller π¨πΌββοΈ , π , π·πΌββοΈ
With VIP as our steadfast ship, we equipped it with three new companions to steer us safely through the perils of app development:
Sailing Smoothly with Captain Coordinator :
Picture Captain Coordinator as the lighthouse guiding our VIP ship through the turbulent app development waters. Just like a seasoned navigator, Captain Coordinator managed our app's flow with finesse, ensuring each transition between screens was seamless and delightful. With Captain Coordinator at the helm, our team could focus on the individual features and logic within each screen without worrying about the complexities of navigation. This freed us from the burden of managing navigation directly within the View or Presenter. Instead, Captain Coordinator took charge, creating a clear separation of concerns and keeping our codebase clean and organized.
Whether it was a simple screen-to-screen transition or a more complex flow involving multiple screens, Captain Coordinator had it all under control. It orchestrated the interactions between View, Presenter, and the other components, allowing our team to concentrate on their specific responsibilities and create a harmonious app development symphony.
The Clever Monkey: Callback Magic at Play π:
Ah, the Clever Monkey, a true acrobat in our VIP circus! With its bag of callback tricks, the Monkey showcased its ingenuity in isolating the View from direct dependencies. As the Monkey handed out callbacks to the View, it performed a clever sleight of hand, transforming the View into a lightweight, nimble performer. By using callbacks, the Clever Monkey empowered the View to remain focused solely on UI rendering and user interactions. The View became a dummy, merely observing and waiting for cues from the Monkey. As a result, we achieved a better separation of concerns and reduced the likelihood of the View becoming overloaded with responsibilities. The Monkey's callback magic not only made our codebase cleaner but also improved testability. With the View decoupled from the Presenter and other components, we could easily write unit tests for specific functionalities without having to deal with complex dependencies.
The Versatile Controller: Master Decision Maker π·πΌββοΈ :
Enter the Versatile Controller, our Jack-of-all-trades VIP component! The Controller took on the responsibility of assigning callbacks to the View, playing the role of a master decision maker in our app's interactions. When the Controller assigned a callback to the View, it imbued the View with an intelligent way to communicate with the rest of the VIP crew. The View, in its dummy state, would call back to the Controller when any action occurred, leaving the Controller in charge of handling the event. Here's where the Controller showcased its versatility. Depending on the nature of the callback event, the Controller made informed decisions: if the action required a network call, the Controller engaged the Interactor to carry out the task efficiently. On the other hand, if the callback warranted a navigation change, the Controller seamlessly called upon Captain Coordinator to navigate the ship to the intended destination. With its masterful decision-making prowess, the Versatile Controller simplified our development process and eliminated the need for intricate conditional logic within the View or Presenter.
Our VIP Crew in Action πͺ :
Captain Coordinator:
Dependencies [Provided by parent's Coordinator (The coordinator of the previous view)] :-
A Navigation Controller : used to push views.
A Callback closure (with an enum as an argument) AKA a Monkey π to communicate backwards from the child view to the parent view
private func openForgotPassword() {
var forgetPasswordCoordinator: ForgetPasswordCoordinatorProtocol? = nil
struct ForgetPasswordUseCase: ForgetPasswordCoordinatorUseCaseProtocol {
var navigationController: UINavigationController
var callback: ForgetPasswordCoordinatorCall
}
let useCase = ForgetPasswordUseCase(navigationController: navigationController) { [weak self] callbackType in
switch callbackType {
case .close:
self?.dependencies.removeElementByReference(forgetPasswordCoordinator)
}
}
forgetPasswordCoordinator = ForgetPasswordCoordinator(useCase: useCase)
forgetPasswordCoordinator?.start()
dependencies.append(forgetPasswordCoordinator!)
}
Responsibilities :-
Creates:
Controller
Presenter
Interactor
View [UIViewController]
required init(useCase: ForgetPasswordCoordinatorUseCaseProtocol) {
navigationController = useCase.navigationController
topViewController = useCase.navigationController.topViewController
self.interactor.presenter = self.presenter
self.controller = ForgetPasswordController(interactor: interactor, callback: processControllerCallback())
self.callback = useCase.callback
self.presenter.controller = self.controller
}
Pushes the view controller using the passed UINavigationController to the screen
In case of wanting to push a child view , it creates the child view's Coordinator and provides it's dependencies and calls it's start function
func openAddCardFlow() {
var addCardCoordinator: (AddExternalCardCoordinatorProtocol & BaseCoordinatorProtocol)? = nil
struct UseCase: AddExternalCardCoordinatorUseCaseProtocol {
var navigationController: UINavigationController
var callback: AddExternalCardCoordinatorCall
}
let useCase = UseCase(navigationController: navigationController, callback: { [weak self] callback in
switch callback {
case .close:
self?.dependencies.removeElementByReference(addCardCoordinator)
}
})
addCardCoordinator = AddExternalCardCoordinator(useCase: useCase)
addCardCoordinator?.start()
dependencies.append(addCardCoordinator!)
}
start() function: pushes the self view to the screen using the passed navigation controller
func start() {
controller?.view = rootView
self.navigationController.pushViewController(rootView, animated: true)
}
View:
Dependencies :-
- uses a closure AKA a Monkey π passed by the controller to transfer actions logic from the view to the controller using map and passing the state to this closure using an enum
weak var view: NewHomeViewProtocol? {
didSet {
view?.callback = processViewCallback()
}
}
Responsibilities :-
- Creates UI components
setupUI() : set UI constraints and customization
func setupUI() {
self.titleLabel.text = NewLoginResourses.Text.title
self.titleDescription.text = NewLoginResourses.Text.titleDescription
self.nextButton.setTitle(NewLoginResourses.Text.login, for: .normal)}
display() : is called from the controller to display data on the view
func display(viewModel: NewHomeViewModelProtocol) {
let image = viewModel.hasNotifications ? NewHomeResourses.Image.bellNotifications : NewHomeResourses.Image.bell
notificationButton.setImage(image, for: .normal)
nameLabel.text = viewModel.userName
}
setupActions(): set action events that occurs in the view
func setupActions() {
let pan = UIPanGestureRecognizer.init(target: self, action: #selector(pan(gesture:)))
self.transactionContainerView.addGestureRecognizer(pan)
activateCardButton.action = { [weak self] in
self?.callback?(.activateCard)
}
}
The Controller:
Dependencies :-
- interactor : Provided by coordinators
self.controller = NewLoginController(interactor: interactor, callback: processControllerCallback())
- callback closure to interact with the coordinator : Provided by the coordinator
self.controller = NewLoginController(interactor: interactor, callback: processControllerCallback())
- weak reference of the view : to assign a closure to the view from which the view can interact with the controller
weak var view: NewHomeViewProtocol? { didSet { view?.callback = processViewCallback() } }
Responsibilities :-
- takes the UI events from the view and decides what to do with it :
if the action requires navigating to another view then the controller tells the coordinator using the closure passed by the coordinator to it
if a request call needs to be fired then the controller interacts with the interactor to fire the request.
func processViewCallback() -> NewLoginViewCallback {
return {[weak self] type in
switch type {
case .back:
self?.callback(.close)
case .openForgotPassword:
self?.callback(.openForgotPassword)
case .openRegistration:
self?.callback(.openRegistration)
case .next(let countryCode, let phoneNumber, let password):
self?.interactor.login(countryCode: countryCode, phoneNumber: phoneNumber, password: password)
}
}
}
Interactor:
Dependencies :-
- Presenter : Provided by coordinators
self.interactor.presenter = self.presenter
- one or more workers : Provided by a NetworkService
private let loginWorker = AppDelegateProvider().forceProvide().appBuilder.nextaNetworkService.loginWorker
private let userDetailsWorker = AppDelegateProvider().forceProvide().appBuilder.nextaNetworkService.userDetailsWorker
Responsibilities :-
- Uses workers to fire the network requests and then goes back to the presenter with the action to do using a protocol
func confirmMobile(otp: String) {
guard let _phoneNumber = self.phone?.formatted else { return }
registrationWorker.sendOTPCode(phoneNumber: _phoneNumber, otpCode: otp) { [weak self] result in
switch result {
case .success(let token):
self?.registrationToken = token
self?.presenter?.processValidationPhoneNumber(error: nil)
self?.createLocalUser()
case .failure(let error):
self?.presenter?.processValidationPhoneNumber(error: error)
}
}
}
Presenter:
Dependencies :-
- Controller: Provided by the coordinator
self.presenter.controller = self.controller
Responsibilities :-
- Manipulates the view using the controller's displayOnView() functions.
switch _error {
case .noInternetConnection:
controller?.displayOnView(viewModel: ViewModel(anotherError: AppText.Common.noInternetConnection))
}
Conclusion: A Harmonious Symphony of VIP Components
As we reflect on our VIP adventure, we're humbled by the harmony achieved with Captain Coordinator, the Clever Monkey, and the Versatile Controller. Each component played a crucial role in orchestrating our app's development journey, allowing us to navigate through challenges with ease and confidence.
With the VIP pattern as our guiding star and these new companions on board, our app development voyage became a thrilling, rewarding experience. Captain Coordinator, the Clever Monkey, and the Versatile Controller made the VIP ship sail smoothly, ensuring our codebase remained clean, modular, and easy to maintain.
So, fellow developers, we encourage you to hoist the VIP flag with pride and embrace these VIP crew members. Sail smoothly with Captain Coordinator, witness the Clever Monkey's callback magic at play, and let the Versatile Controller be your master decision maker as you embark on your own exciting VIP app development journey. Bon voyage, and may your VIP ship always sail towards success and beyond! π’πβοΈ