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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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
- Create a new Xcode project (Single View App, Swift, UIKit).
- Add the files as shown in the project structure.
- Build and run the app.
- 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: