Skip to content

VIPER Architecture with Reducer and State ​

June 14, 2025

This blog dives into VIPER architecture for iOS, enhanced with Redux-inspired Reducer and State for predictable state management. It features a Swift-based Task List app example, showcasing modular design, unit tests, and setup instructions for iOS developers.

Introduction to VIPER with Reducer and State ​

VIPER (View, Interactor, Presenter, Entity, Router) is a clean architecture pattern for iOS that separates concerns into modular components. By integrating Reducer and State concepts, inspired by Redux, we add a predictable state management layer to VIPER. This approach centralizes state changes through actions and a reducer function, making the app's behavior more predictable and easier to test.

  • View: Displays the UI and forwards user interactions to the Presenter.
  • Interactor: Handles business logic and dispatches actions to the Reducer.
  • Presenter: Prepares data for the View and interprets State changes.
  • Entity: Represents data models.
  • Router: Manages navigation.
  • State: A single source of truth for the app's data at a given time.
  • Reducer: A pure function that takes the current State and an Action to produce a new State.

Benefits ​

  • Predictable State: All state changes are handled by the Reducer, ensuring consistency.
  • Testability: Reducer and State are pure and isolated, making them easy to test.
  • Modularity: Combines VIPER’s clean architecture with centralized state management.
  • Scalability: Simplifies adding new features by extending the State and Reducer.

Example Project: Task List App with Reducer and State ​

This example builds a Task List app where users can add, delete, and view tasks, using VIPER with a Reducer and State to manage the task list state.

Project Structure ​

TaskListApp/
├── Models/
│   └── Task.swift
├── StateManagement/
│   ├── TaskListState.swift
│   ├── TaskListActions.swift
│   ├── TaskListReducer.swift
├── TaskListModule/
│   ├── Contracts/
│   │   └── TaskListContracts.swift
│   ├── View/
│   │   └── TaskListViewController.swift
│   ├── Presenter/
│   │   └── TaskListPresenter.swift
│   ├── Interactor/
│   │   └── TaskListInteractor.swift
│   ├── Router/
│   │   └── TaskListRouter.swift
│   └── Entity/
│       └── TaskEntity.swift
└── AppDelegate.swift

Step 1: Define the Entity ​

The Task model represents the data structure.

swift
// Task.swift
struct Task {
    let id: String
    let title: String
    let isCompleted: Bool
}

Step 2: Define State, Actions, and Reducer ​

The State holds the app’s data, Actions define state changes, and the Reducer processes actions to update the State.

swift
// TaskListState.swift
struct TaskListState {
    var tasks: [Task]
    var error: String?
}

// TaskListActions.swift
enum TaskListAction {
    case fetchTasks
    case addTask(title: String)
    case deleteTask(id: String)
    case setError(message: String)
}

// TaskListReducer.swift
struct TaskListReducer {
    static func reduce(state: TaskListState, action: TaskListAction) -> TaskListState {
        var newState = state
        switch action {
        case .fetchTasks:
            // In a real app, fetch from a data source
            newState.error = nil
            return newState
        case .addTask(let title):
            let newTask = Task(id: UUID().uuidString, title: title, isCompleted: false)
            newState.tasks.append(newTask)
            newState.error = nil
            return newState
        case .deleteTask(let id):
            newState.tasks.removeAll { $0.id == id }
            newState.error = nil
            return newState
        case .setError(let message):
            newState.error = message
            return newState
        }
    }
}

Step 3: Define Contracts ​

Contracts define interfaces for VIPER components, updated to include State and Action interactions.

swift
// TaskListContracts.swift
protocol TaskListViewProtocol: AnyObject {
    func displayTasks(_ tasks: [Task])
    func showError(_ message: String)
}

protocol TaskListPresenterProtocol {
    func viewDidLoad()
    func didTapAddTask(title: String)
    func didTapDeleteTask(id: String)
}

protocol TaskListInteractorInputProtocol {
    func dispatch(_ action: TaskListAction)
    func getState() -> TaskListState
}

protocol TaskListInteractorOutputProtocol: AnyObject {
    func didUpdateState(_ state: TaskListState)
}

protocol TaskListRouterProtocol {
    static func createTaskListModule() -> UIViewController
}

Step 4: Implement the View ​

The View displays tasks and forwards user interactions to the Presenter.

swift
// TaskListViewController.swift
import UIKit

class TaskListViewController: UIViewController, TaskListViewProtocol {
    var presenter: TaskListPresenterProtocol?
    private var tableView: UITableView!
    private var tasks: [Task] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        presenter?.viewDidLoad()
    }
    
    private func setupUI() {
        view.backgroundColor = .white
        title = "Task List"
        
        navigationItem.rightBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .add,
            target: self,
            action: #selector(addTaskTapped)
        )
        
        tableView = UITableView(frame: view.bounds)
        tableView.dataSource = self
        tableView.delegate = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "TaskCell")
        view.addSubview(tableView)
    }
    
    @objc private func addTaskTapped() {
        let alert = UIAlertController(title: "Add Task", message: nil, preferredStyle: .alert)
        alert.addTextField { textField in
            textField.placeholder = "Task Title"
        }
        alert.addAction(UIAlertAction(title: "Add", style: .default) { _ in
            if let title = alert.textFields?.first?.text, !title.isEmpty {
                self.presenter?.didTapAddTask(title: title)
            }
        })
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        present(alert, animated: true)
    }
    
    func displayTasks(_ tasks: [Task]) {
        self.tasks = tasks
        tableView.reloadData()
    }
    
    func showError(_ message: String) {
        let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }
}

extension TaskListViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tasks.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TaskCell", for: indexPath)
        let task = tasks[indexPath.row]
        cell.textLabel?.text = task.title
        cell.accessoryType = task.isCompleted ? .checkmark : .none
        return cell
    }
    
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            let task = tasks[indexPath.row]
            presenter?.didTapDeleteTask(id: task.id)
        }
    }
}

Step 5: Implement the Presenter ​

The Presenter interprets the State and updates the View.

swift
// TaskListPresenter.swift
class TaskListPresenter: TaskListPresenterProtocol {
    weak var view: TaskListViewProtocol?
    var interactor: TaskListInteractorInputProtocol?
    var router: TaskListRouterProtocol?
    
    func viewDidLoad() {
        interactor?.dispatch(.fetchTasks)
    }
    
    func didTapAddTask(title: String) {
        interactor?.dispatch(.addTask(title: title))
    }
    
    func didTapDeleteTask(id: String) {
        interactor?.dispatch(.deleteTask(id: id))
    }
}

extension TaskListPresenter: TaskListInteractorOutputProtocol {
    func didUpdateState(_ state: TaskListState) {
        view?.displayTasks(state.tasks)
        if let error = state.error {
            view?.showError(error)
        }
    }
}

Step 6: Implement the Interactor ​

The Interactor manages the State and Reducer, dispatching actions and notifying the Presenter of state changes.

swift
// TaskListInteractor.swift
class TaskListInteractor: TaskListInteractorInputProtocol {
    weak var presenter: TaskListInteractorOutputProtocol?
    private var state: TaskListState
    
    init() {
        self.state = TaskListState(tasks: [], error: nil)
    }
    
    func dispatch(_ action: TaskListAction) {
        // Simulate async operation (e.g., API call)
        DispatchQueue.global().async {
            let newState = TaskListReducer.reduce(state: self.state, action: action)
            self.state = newState
            DispatchQueue.main.async {
                self.presenter?.didUpdateState(newState)
            }
        }
    }
    
    func getState() -> TaskListState {
        return state
    }
}

Step 7: Implement the Router ​

The Router sets up the module and handles navigation.

swift
// TaskListRouter.swift
import UIKit

class TaskListRouter: TaskListRouterProtocol {
    static func createTaskListModule() -> UIViewController {
        let view = TaskListViewController()
        let presenter = TaskListPresenter()
        let interactor = TaskListInteractor()
        let router = TaskListRouter()
        
        view.presenter = presenter
        presenter.view = view
        presenter.interactor = interactor
        presenter.router = router
        interactor.presenter = presenter
        
        return view
    }
}

Step 8: Set Up AppDelegate ​

Configure the app to display the Task List module.

swift
// AppDelegate.swift
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        let navigationController = UINavigationController(rootViewController: TaskListRouter.createTaskListModule())
        window?.rootViewController = navigationController
        window?.makeKeyAndVisible()
        return true
    }
}

Step 9: Testing the Reducer ​

The Reducer’s purity makes it easy to test with XCTest.

swift
// TaskListReducerTests.swift
import XCTest
@testable import TaskListApp

class TaskListReducerTests: XCTestCase {
    var state: TaskListState!
    
    override func setUp() {
        super.setUp()
        state = TaskListState(tasks: [], error: nil)
    }
    
    func testAddTask() {
        let action = TaskListAction.addTask(title: "Test Task")
        let newState = TaskListReducer.reduce(state: state, action: action)
        
        XCTAssertEqual(newState.tasks.count, 1)
        XCTAssertEqual(newState.tasks.first?.title, "Test Task")
        XCTAssertNil(newState.error)
    }
    
    func testDeleteTask() {
        let task = Task(id: "1", title: "Test Task", isCompleted: false)
        state = TaskListState(tasks: [task], error: nil)
        let action = TaskListAction.deleteTask(id: "1")
        let newState = TaskListReducer.reduce(state: state, action: action)
        
        XCTAssertEqual(newState.tasks.count, 0)
        XCTAssertNil(newState.error)
    }
    
    func testSetError() {
        let action = TaskListAction.setError(message: "Test Error")
        let newState = TaskListReducer.reduce(state: state, action: action)
        
        XCTAssertEqual(newState.error, "Test Error")
        XCTAssertEqual(newState.tasks.count, 0)
    }
}

Step 10: Running the App ​

  1. Create a new Xcode project (Single View App, Swift, UIKit).
  2. Add the files as shown in the project structure.
  3. Build and run the app.
  4. The app displays a task list, allows adding tasks via an alert, and supports deleting tasks by swiping.

Key Differences from Standard VIPER ​

  • Centralized State: The TaskListState struct holds all relevant data, making state predictable.
  • Reducer: The TaskListReducer handles state transitions based on actions, ensuring no side effects.
  • Interactor Role: The Interactor now dispatches actions and manages the State, reducing direct manipulation of data.

Conclusion ​

Integrating Reducer and State into VIPER enhances state management by making it centralized and predictable. This Task List app demonstrates how to combine VIPER’s modularity with Redux-like state management. You can extend this by adding persistence (e.g., Core Data) or more complex actions.

For further details, refer to:

Released under the MIT License.