Setting Sail with VIP: An Epic Adventure in iOS App Development!

Setting Sail with VIP: An Epic Adventure in iOS App Development!

Β·

9 min read

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! πŸš’πŸ’βš“οΈ

Β