agentsclimarketplace

Arimunandar vip architecture

Skill JordanCoin/ios-skills-collection/skills/arimunandar--vip-architecture

VIP+W (View-Interactor-Presenter-Worker) Clean Architecture for iOS with unidirectional data flow and proper separation of concernsFrom its SKILL.md

Install
npx -y skills add JordanCoin/ios-skills-collection --skill arimunandar--vip-architecture

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

19.4 KB, ~3.9k tokens by cl100k_base, as published. Nobody here has run it

VIP+W Clean Architecture

A scalable, testable architecture pattern for iOS apps with strict separation of concerns and unidirectional data flow.

Components Overview

View (ViewController)

  • Responsibility: Display logic ONLY
  • Protocol: DisplayLogic
  • Does: Renders UI, forwards user actions to Interactor
  • Does NOT: Contain business logic, format data, make API calls

Interactor

  • Responsibility: Orchestrates business logic
  • Protocol: BusinessLogic
  • Does: Receives requests from View, coordinates Workers, sends responses to Presenter
  • Does NOT: Format data for display, handle UI, make direct API calls

Presenter

  • Responsibility: Formats data for display
  • Protocol: PresentationLogic
  • Does: Transforms business data into ViewModels, calls View methods
  • Does NOT: Contain business logic, hold state, access network

Worker

  • Responsibility: Handles async operations
  • Protocol: WorkerLogic
  • Does: API calls, database operations, heavy computations
  • Does NOT: Know about View or Presenter, contain business decisions

Router/Wireframe

  • Responsibility: Navigation between scenes
  • Protocol: RoutingLogic, DataPassingProtocol
  • Does: Push/present view controllers, pass data between scenes
  • Does NOT: Contain business logic

Models

  • Request: Data sent from View to Interactor
  • Response: Data sent from Interactor to Presenter
  • ViewModel: Data sent from Presenter to View

Data Flow (Unidirectional)

┌───────────────────────────────────────────────────────────────┐
│                                                               │
│   View ──request──► Interactor ──delegate──► Worker          │
│     ▲                    │                      │             │
│     │                    │◄─────result──────────┘             │
│     │                    │                                    │
│     │                    ▼                                    │
│     └──viewModel──── Presenter                                │
│                                                               │
│   Router: Navigation between scenes (accessed via Interactor) │
│                                                               │
└───────────────────────────────────────────────────────────────┘

File Structure Template

SceneName/
├── SceneNameViewController.swift    # View (DisplayLogic)
├── SceneNameInteractor.swift        # Business Logic (BusinessLogic)
├── SceneNamePresenter.swift         # Format for Display (PresentationLogic)
├── SceneNameWorker.swift            # Async Operations (WorkerLogic)
├── SceneNameRouter.swift            # Navigation (RoutingLogic)
├── SceneNameModels.swift            # Request/Response/ViewModel
└── SceneNameConfigurator.swift      # DI Assembly

Protocol Definitions

DisplayLogic (View conforms)

protocol LoginDisplayLogic: AnyObject {
    func displayLoginResult(viewModel: Login.Authenticate.ViewModel)
    func displayValidationError(viewModel: Login.Validate.ViewModel)
    func displayLoading(isLoading: Bool)
}

BusinessLogic (Interactor conforms)

protocol LoginBusinessLogic {
    func authenticate(request: Login.Authenticate.Request)
    func validateInput(request: Login.Validate.Request)
}

PresentationLogic (Presenter conforms)

protocol LoginPresentationLogic {
    func presentLoginResult(response: Login.Authenticate.Response)
    func presentValidationError(response: Login.Validate.Response)
    func presentLoading(isLoading: Bool)
}

WorkerLogic (Worker conforms)

protocol LoginWorkerLogic {
    func login(email: String, password: String) async throws -> User
    func validateCredentials(email: String, password: String) -> ValidationResult
}

RoutingLogic (Router conforms)

protocol LoginRoutingLogic {
    func routeToHome()
    func routeToForgotPassword()
    func routeToSignup()
}

protocol LoginDataPassing {
    var dataStore: LoginDataStore? { get }
}

DataStore (Shared state between scenes)

protocol LoginDataStore {
    var authenticatedUser: User? { get set }
}

Complete Code Templates

Models (SceneNameModels.swift)

import Foundation

enum Login {

    // MARK: - Use Cases

    enum Authenticate {
        struct Request {
            let email: String
            let password: String
        }

        struct Response {
            let result: Result<User, AuthError>
        }

        struct ViewModel {
            let isSuccess: Bool
            let errorMessage: String?
            let welcomeMessage: String?
        }
    }

    enum Validate {
        struct Request {
            let email: String
            let password: String
        }

        struct Response {
            let emailError: String?
            let passwordError: String?
        }

        struct ViewModel {
            let emailErrorText: String?
            let passwordErrorText: String?
            let isValid: Bool
        }
    }
}

Worker (SceneNameWorker.swift)

import Foundation

protocol LoginWorkerLogic {
    func login(email: String, password: String) async throws -> User
    func validateCredentials(email: String, password: String) -> ValidationResult
}

final class LoginWorker: LoginWorkerLogic {

    // MARK: - Dependencies

    private let apiClient: APIClientProtocol
    private let validator: CredentialValidatorProtocol

    // MARK: - Init

    init(
        apiClient: APIClientProtocol = APIClient.shared,
        validator: CredentialValidatorProtocol = CredentialValidator()
    ) {
        self.apiClient = apiClient
        self.validator = validator
    }

    // MARK: - WorkerLogic

    func login(email: String, password: String) async throws -> User {
        let request = LoginRequest(email: email, password: password)
        let response: LoginResponse = try await apiClient.perform(request)
        return response.user
    }

    func validateCredentials(email: String, password: String) -> ValidationResult {
        validator.validate(email: email, password: password)
    }
}

struct ValidationResult {
    let emailError: String?
    let passwordError: String?

    var isValid: Bool {
        emailError == nil && passwordError == nil
    }
}

Interactor (SceneNameInteractor.swift)

import Foundation

protocol LoginBusinessLogic {
    func authenticate(request: Login.Authenticate.Request)
    func validateInput(request: Login.Validate.Request)
}

protocol LoginDataStore {
    var authenticatedUser: User? { get set }
}

final class LoginInteractor: LoginBusinessLogic, LoginDataStore {

    // MARK: - Dependencies

    var presenter: LoginPresentationLogic?
    var worker: LoginWorkerLogic

    // MARK: - DataStore

    var authenticatedUser: User?

    // MARK: - Init

    init(worker: LoginWorkerLogic = LoginWorker()) {
        self.worker = worker
    }

    // MARK: - BusinessLogic

    func authenticate(request: Login.Authenticate.Request) {
        presenter?.presentLoading(isLoading: true)

        Task { @MainActor in
            defer { presenter?.presentLoading(isLoading: false) }

            do {
                let user = try await worker.login(
                    email: request.email,
                    password: request.password
                )
                authenticatedUser = user
                let response = Login.Authenticate.Response(result: .success(user))
                presenter?.presentLoginResult(response: response)
            } catch let error as AuthError {
                let response = Login.Authenticate.Response(result: .failure(error))
                presenter?.presentLoginResult(response: response)
            } catch {
                let response = Login.Authenticate.Response(result: .failure(.unknown))
                presenter?.presentLoginResult(response: response)
            }
        }
    }

    func validateInput(request: Login.Validate.Request) {
        let result = worker.validateCredentials(
            email: request.email,
            password: request.password
        )
        let response = Login.Validate.Response(
            emailError: result.emailError,
            passwordError: result.passwordError
        )
        presenter?.presentValidationError(response: response)
    }
}

Presenter (SceneNamePresenter.swift)

import Foundation

protocol LoginPresentationLogic {
    func presentLoginResult(response: Login.Authenticate.Response)
    func presentValidationError(response: Login.Validate.Response)
    func presentLoading(isLoading: Bool)
}

final class LoginPresenter: LoginPresentationLogic {

    // MARK: - Dependencies

    weak var viewController: LoginDisplayLogic?

    // MARK: - PresentationLogic

    func presentLoginResult(response: Login.Authenticate.Response) {
        let viewModel: Login.Authenticate.ViewModel

        switch response.result {
        case .success(let user):
            viewModel = Login.Authenticate.ViewModel(
                isSuccess: true,
                errorMessage: nil,
                welcomeMessage: "Welcome, \(user.name)!"
            )

        case .failure(let error):
            viewModel = Login.Authenticate.ViewModel(
                isSuccess: false,
                errorMessage: error.localizedDescription,
                welcomeMessage: nil
            )
        }

        viewController?.displayLoginResult(viewModel: viewModel)
    }

    func presentValidationError(response: Login.Validate.Response) {
        let viewModel = Login.Validate.ViewModel(
            emailErrorText: response.emailError,
            passwordErrorText: response.passwordError,
            isValid: response.emailError == nil && response.passwordError == nil
        )
        viewController?.displayValidationError(viewModel: viewModel)
    }

    func presentLoading(isLoading: Bool) {
        viewController?.displayLoading(isLoading: isLoading)
    }
}

ViewController (SceneNameViewController.swift)

import UIKit

protocol LoginDisplayLogic: AnyObject {
    func displayLoginResult(viewModel: Login.Authenticate.ViewModel)
    func displayValidationError(viewModel: Login.Validate.ViewModel)
    func displayLoading(isLoading: Bool)
}

final class LoginViewController: UIViewController {

    // MARK: - VIP References

    var interactor: LoginBusinessLogic?
    var router: (LoginRoutingLogic & LoginDataPassing)?

    // MARK: - UI Elements

    private let emailTextField = UITextField()
    private let passwordTextField = UITextField()
    private let loginButton = UIButton(type: .system)
    private let activityIndicator = UIActivityIndicatorView(style: .large)
    private let emailErrorLabel = UILabel()
    private let passwordErrorLabel = UILabel()

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupActions()
    }

    // MARK: - Setup

    private func setupUI() {
        view.backgroundColor = .systemBackground
        // Layout code here (see uikit-layouting skill)
    }

    private func setupActions() {
        loginButton.addTarget(self, action: #selector(loginTapped), for: .touchUpInside)
        emailTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
        passwordTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
    }

    // MARK: - Actions

    @objc private func loginTapped() {
        let request = Login.Authenticate.Request(
            email: emailTextField.text ?? "",
            password: passwordTextField.text ?? ""
        )
        interactor?.authenticate(request: request)
    }

    @objc private func textFieldDidChange() {
        let request = Login.Validate.Request(
            email: emailTextField.text ?? "",
            password: passwordTextField.text ?? ""
        )
        interactor?.validateInput(request: request)
    }
}

// MARK: - DisplayLogic

extension LoginViewController: LoginDisplayLogic {

    func displayLoginResult(viewModel: Login.Authenticate.ViewModel) {
        if viewModel.isSuccess {
            router?.routeToHome()
        } else if let errorMessage = viewModel.errorMessage {
            showAlert(title: "Error", message: errorMessage)
        }
    }

    func displayValidationError(viewModel: Login.Validate.ViewModel) {
        emailErrorLabel.text = viewModel.emailErrorText
        passwordErrorLabel.text = viewModel.passwordErrorText
        loginButton.isEnabled = viewModel.isValid
    }

    func displayLoading(isLoading: Bool) {
        loginButton.isEnabled = !isLoading
        if isLoading {
            activityIndicator.startAnimating()
        } else {
            activityIndicator.stopAnimating()
        }
    }

    private func showAlert(title: String, message: String) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
}

Router (SceneNameRouter.swift)

import UIKit

protocol LoginRoutingLogic {
    func routeToHome()
    func routeToForgotPassword()
    func routeToSignup()
}

protocol LoginDataPassing {
    var dataStore: LoginDataStore? { get }
}

final class LoginRouter: LoginRoutingLogic, LoginDataPassing {

    // MARK: - Dependencies

    weak var viewController: LoginViewController?
    var dataStore: LoginDataStore?

    // MARK: - Routing

    func routeToHome() {
        let destinationVC = HomeConfigurator.configure()
        passDataToHome(destination: destinationVC)
        navigateToHome(destination: destinationVC)
    }

    func routeToForgotPassword() {
        let destinationVC = ForgotPasswordConfigurator.configure()
        viewController?.navigationController?.pushViewController(destinationVC, animated: true)
    }

    func routeToSignup() {
        let destinationVC = SignupConfigurator.configure()
        viewController?.navigationController?.pushViewController(destinationVC, animated: true)
    }

    // MARK: - Navigation

    private func navigateToHome(destination: HomeViewController) {
        viewController?.navigationController?.setViewControllers([destination], animated: true)
    }

    // MARK: - Data Passing

    private func passDataToHome(destination: HomeViewController) {
        guard let user = dataStore?.authenticatedUser else { return }
        destination.router?.dataStore?.currentUser = user
    }
}

Configurator (SceneNameConfigurator.swift)

import UIKit

enum LoginConfigurator {

    static func configure() -> LoginViewController {
        let viewController = LoginViewController()
        let interactor = LoginInteractor()
        let presenter = LoginPresenter()
        let router = LoginRouter()
        let worker = LoginWorker()

        // Connect VIP cycle
        viewController.interactor = interactor
        viewController.router = router
        interactor.presenter = presenter
        interactor.worker = worker
        presenter.viewController = viewController
        router.viewController = viewController
        router.dataStore = interactor

        return viewController
    }
}

Worker Responsibilities

API Calls

func fetchUser(id: String) async throws -> User {
    try await apiClient.get("/users/\(id)")
}

Database Operations

func saveUser(_ user: User) async throws {
    try await coreDataManager.save(user)
}

File I/O

func loadCachedData() async throws -> Data {
    try await fileManager.read(from: cacheURL)
}

Heavy Computations

func processImage(_ image: UIImage) async -> UIImage {
    await Task.detached(priority: .userInitiated) {
        image.applyFilters()
    }.value
}

Testing Strategy

Test Interactor with Mock Worker

final class LoginInteractorTests: XCTestCase {
    var sut: LoginInteractor!
    var mockPresenter: MockLoginPresenter!
    var mockWorker: MockLoginWorker!

    override func setUp() {
        mockPresenter = MockLoginPresenter()
        mockWorker = MockLoginWorker()
        sut = LoginInteractor(worker: mockWorker)
        sut.presenter = mockPresenter
    }

    func testAuthenticateSuccess() async {
        mockWorker.loginResult = .success(User.mock)

        sut.authenticate(request: .init(email: "[email protected]", password: "password"))

        await fulfillment(of: [mockPresenter.presentLoginResultCalled])
        XCTAssertTrue(mockPresenter.lastLoginResponse?.result.isSuccess == true)
    }
}

Test Presenter with Mock View

final class LoginPresenterTests: XCTestCase {
    var sut: LoginPresenter!
    var mockView: MockLoginViewController!

    override func setUp() {
        mockView = MockLoginViewController()
        sut = LoginPresenter()
        sut.viewController = mockView
    }

    func testPresentLoginResultSuccess() {
        let response = Login.Authenticate.Response(result: .success(User.mock))

        sut.presentLoginResult(response: response)

        XCTAssertTrue(mockView.lastLoginViewModel?.isSuccess == true)
        XCTAssertNotNil(mockView.lastLoginViewModel?.welcomeMessage)
    }
}

Best Practices

  1. Never skip the Worker - All async operations go through Worker
  2. Keep Presenter pure - No side effects, only data transformation
  3. View is dumb - Only calls interactor?.someMethod(request:)
  4. Use the Configurator - Single place for dependency assembly
  5. Protocol-first - Define protocols before implementations
  6. Test each layer - Mock dependencies for isolation

Pre-Creation Interview

Before creating a new VIP+W scene, gather requirements using AskUserQuestion:

Scene Creation Questions

Question 1: Scene Purpose

  • Header: "Purpose"
  • Question: "What is the main purpose of this scene?"
  • Options:
    • Display data - Show information (list, detail, profile)
    • Form input - Collect user input (login, registration, settings)
    • Navigation hub - Route to other scenes (home, dashboard)
    • Action flow - Multi-step process (checkout, onboarding)

Question 2: Data Requirements

  • Header: "Data"
  • Question: "What data operations does this scene need?"
  • Options:
    • Fetch from API - Load data from backend
    • Local storage - Read/write to database
    • User input only - No external data
    • Multiple sources - API + local + input

Question 3: Navigation Pattern

  • Header: "Navigation"
  • Question: "How does this scene connect to others?"
  • Options:
    • Push detail - Navigate deeper (list → detail)
    • Modal presentation - Overlay screens
    • Replace root - New flow (login → home)
    • Tab child - Part of tab navigation

Interview Flow

  1. Ask questions using AskUserQuestion
  2. Summarize: "Creating a [purpose] scene with [data] operations using [navigation] pattern"
  3. Generate file structure
  4. Create protocols first
  5. Implement each component

Skip Interview If:

  • User provided detailed scene requirements
  • User says "skip questions" or "just do it"
  • Scene purpose is obvious from context

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Gives 0 of the 12 instructions most architecture codebase skills give in ~3.9k tokens

Counted across 811 of the 1,134 authors here whose files we hold, read 2026-08-07

  • Ask the user which candidate to explorein 45 of 811, across 15 files
  • Apply the deletion test to suspected shallow modulesin 43 of 811, across 15 files
  • Read any relevant architecture decision records firstin 31 of 811, across 8 files
  • Use exact glossary terms in every suggestionin 30 of 811, across 10 files
  • Accept dependencies instead of creating themin 24 of 811, across 5 files
  • Include before and after visualisations for each candidatein 24 of 811, across 5 files
  • Read the domain glossary before exploringin 24 of 811, across 6 files
  • Return results instead of producing side effectsin 23 of 811, across 4 files
  • Explore the codebase for shallow modules and frictionin 23 of 811, across 3 files
  • Introduce seams only where things varyin 22 of 811, across 3 files
  • Reduce the number of methodsin 21 of 811, across 2 files
  • Design deep modules with small interfacesin 21 of 811, across 3 files

Said here and by no other author read

  • Forward user requests from View to Interactor
  • Delegate async operations from Interactor to Worker
  • Send results from Interactor to Presenter
  • Format business data into ViewModels in Presenter
  • Handle navigation between scenes in the Router
  • Define Request, Response, and ViewModel data structs

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 326,782. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.