Hello! Welcome back to our module on Advanced Architecture and Navigation.
In our last lesson, we established a theoretical foundation for VIPER, describing the distinct roles of the View, Interactor, Presenter, Entity, and Router. We learned that the Interactor is designated to handle the business logic for a specific use case, completely separate from the UI.
Today, we're moving from theory to practice. Our goal is to implement a single VIPER component, the Interactor, to encapsulate business logic. By the end of this lesson, you'll not only have a clear code-level example of an Interactor but also a solid framework for deciding what logic belongs there in the first place. This is a key step toward building the highly modular and testable applications you're aiming for.
What is "Business Logic"? The Interactor's Domain
Before we write any code, we need to be very clear about what we mean by "business logic" and why it belongs in the Interactor. A common point of confusion in VIPER is the dividing line between the Presenter's responsibilities and the Interactor's.
The Interactor deals with use cases. A use case is a specific task the system performs that often involves creating, retrieving, or manipulating your app's core data models (Entities). This is distinct from presentation logic (like formatting a date for a label), which belongs in the Presenter.
To help clarify this distinction, let's explore a helpful article that provides a simple framework for making this decision.
iOS VIPER: Presenter x Interactor responsabilities
This article by Pedro Alvarez, titled 'iOS VIPER: Presenter x Interactor responsabilities', does an excellent job of tackling this exact problem. It offers a practical guide for separating these two layers.
Please read the sections 'What are use cases?' and 'Feature flags and project configuration'. As you read, focus on the three key questions the author proposes to determine if a task belongs to the Interactor.
As the article highlights, you can determine if a piece of logic is a "use case" for the Interactor by asking these three questions:
- Does the operation manage a core business data model (Entity)?
- Does it need to fetch data from an external source (e.g., a web API, a database)?
- Does it involve data logic that is not related to how the UI will display that data (e.g., complex calculations, parsing, validation against a backend rule)?
If you answer YES to any of these, the logic belongs in the Interactor.
For example, consider a sign-up form:
- Validating if an email field has a valid format (
@,.com) for UI feedback (e.g., turning a text field border red): This is presentation logic. It's about how the UI behaves. This belongs in the Presenter. - Taking the validated email and password and making a network request to a server to create a new user account: This is a use case. It involves an external source (the API) and manipulates business data (the new user). This belongs in the Interactor.
Implementing an Interactor: A Practical Example
Now, let's see what this looks like in code. We'll walk through the implementation of a UserInteractor whose job is to fetch a list of users from a public API. This is a classic Interactor responsibility.
The video below builds a complete VIPER module from scratch. We will focus specifically on the parts that define and implement the Interactor.
Swift: VIPER Design Pattern (Architecture, 2023, Xcode 14, Swift 5) - iOS Development
The 'Swift: VIPER Design Pattern' video from iOS Academy provides a clear, step-by-step implementation. We will use it to see how the Interactor is built and how it communicates with the Presenter.
First, watch the section from 03:23 to 12:51 to understand how all the protocols for the VIPER components are defined. Pay attention to the protocols for the Interactor and Presenter. Then, watch the implementation of the Interactor's business logic from 21:34 to 25:39. Focus on: How the Interactor holds a reference to the Presenter. How the network call (URLSession) is fully contained within the Interactor. How the Interactor communicates the result (success or failure) back to the Presenter.
Let's break down the key parts of that implementation.
1. The Interactor's Contract (Protocol)
First, a protocol defines what the Interactor can do. The Presenter will communicate with the Interactor using this protocol.
protocol AnyInteractor {
var presenter: AnyPresenter? { get set }
func getUsers()
}
- The Interactor holds a reference to the Presenter to send results back. Note that this reference should be
weakto prevent a retain cycle, as the Presenter will hold a strong reference to the Interactor. - The
getUsers()function is the entry point for the use case, which the Presenter will call.
2. The Interactor's Implementation
The concrete class implements the business logic. In this case, it's fetching and decoding data.
class UserInteractor: AnyInteractor {
var presenter: AnyPresenter?
func getUsers() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else { return }
let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
guard let data = data, error == nil else {
// On failure, tell the presenter
self?.presenter?.interactorDidFetchUsers(with: .failure(FetchError.failed))
return
}
do {
let entities = try JSONDecoder().decode([User].self, from: data)
// On success, tell the presenter
self?.presenter?.interactorDidFetchUsers(with: .success(entities))
} catch {
self?.presenter?.interactorDidFetchUsers(with: .failure(error))
}
}
task.resume()
}
}
Notice how the Interactor is completely self-contained. It performs the network request and decoding. It has no knowledge of UIKit, UITableView, or how the data will be displayed. Its only job is to get the data and hand it off to the Presenter.
3. The Handoff to the Presenter
The Interactor communicates its result back to the Presenter using the Presenter's protocol.
// In the Presenter's protocol
protocol AnyPresenter {
// ... other properties and functions
func interactorDidFetchUsers(with result: Result<[User], Error>)
}
// In the Presenter's implementation
class UserPresenter: AnyPresenter {
// ...
func interactorDidFetchUsers(with result: Result<[User], Error>) {
switch result {
case .success(let users):
// Tell the view to update with data
view?.update(with: users)
case .failure:
// Tell the view to show an error
view?.update(with: "Something went wrong")
}
}
}
This completes the cycle: Presenter asks Interactor for data -> Interactor performs business logic -> Interactor gives result back to Presenter -> Presenter tells View what to display.
The Payoff: Testability
Why go through all this trouble? One of the biggest benefits is testability, a key area you want to strengthen. Because the Interactor contains pure business logic with no UI dependencies, you can test it in isolation.
The following article contains a great example of how you would write a unit test for an Interactor.
Building Scalable iOS Apps with VIPER Architecture in Swift
Let's look at the 'Building Scalable iOS Apps with VIPER Architecture in Swift' article. It provides a concise example of unit testing an Interactor.
Read the code block under 'Testing VIPER Apps' in the subsection 'Isolated Unit Tests'. You don't need to implement it now, just understand the concept.
The example test looks something like this:
func testFetchForecast() async {
// 1. Arrange: Create a mock service that returns predictable data
let mockService = MockWeatherService()
let interactor = WeatherInteractor(service: mockService)
// 2. Act: Call the Interactor's method
let forecast = try? await interactor.fetchForecast(for: "test")
// 3. Assert: Verify the result is what you expect
XCTAssertEqual(forecast?.count, 5)
}
By injecting a MockWeatherService, you can test the WeatherInteractor's logic without making a real network call. This makes your tests fast, reliable, and independent of external factors. We will cover this in much more detail in the upcoming modules on testing.
Conclusion
In this lesson, you've bridged the gap from VIPER theory to practice by focusing on its core business logic component: the Interactor.
Key Takeaways:
- An Interactor encapsulates a single use case or piece of business logic.
- Logic belongs in the Interactor if it involves manipulating Entities, interacting with external data sources (like an API), or performing non-UI-related data processing.
- The Interactor is triggered by the Presenter, performs its task, and communicates the result back to the Presenter via a protocol.
- This strict separation makes the Interactor highly testable, as its logic can be verified independently of the UI and other dependencies.
Preview of the Next Lesson:
You now have practical experience with components from several architectures (MVC, MVVM, Coordinator, VIPER). But when should you use one over the other? In the final lesson of this module, we will compare the trade-offs of MVVM, Coordinator, and VIPER, helping you develop the architectural judgment to choose the right pattern for the right job.