VIPER Architecture with SwiftUI: A Comprehensive Guide

 

VIPER (View, Interactor, Presenter, Entity, Router) is a clean architecture pattern designed to make large-scale applications modular, testable, and scalable. SwiftUI, Apple’s declarative UI framework, provides a modern way to build user interfaces. In this guide, we will implement VIPER with SwiftUI by creating a simple app that displays a list of users fetched from a mock API, with navigation to a detail screen.

Step 1: Define the Components

1. Entity (User)

The Entity represents the data model. Here, we define a User struct that conforms to Codable and Identifiable.

struct User: Codable, Identifiable {
let id: Int
let name: String
let email: String
}

2. Router (UserListRouter)

The Router handles navigation. In SwiftUI, navigation is managed using NavigationStack and NavigationLink. Here, the Router provides a method to create the UserDetailView.

import SwiftUI

final class UserListRouter {
static func makeUserDetailView(user: User) -> some View {
UserDetailView(user: user)
}
}

3. Interactor (UserListInteractor)

The Interactor is responsible for business logic and data fetching. In this example, we simulate an API call with mock data.

protocol UserListInteracting {
func fetchUsers(completion: @escaping ([User]) -> Void)
}

final class UserListInteractor: UserListInteracting {
func fetchUsers(completion: @escaping ([User]) -> Void) {
let users = [
User(id: 1, name: "John Doe", email: "john@example.com"),
User(id: 2, name: "Jane Smith", email: "jane@example.com")
]
completion(users)
}
}

4. Presenter (UserListPresenter)

The Presenter fetches data from the Interactor and updates the View using @Published properties.

import SwiftUI
import Combine

protocol UserListPresenting: ObservableObject {
var users: [User] { get }
func viewDidLoad()
}
final class UserListPresenter: UserListPresenting {
@Published var users: [User] = []
private let interactor: UserListInteracting
private let router: UserListRouter
init(interactor: UserListInteracting, router: UserListRouter) {
self.interactor = interactor
self.router = router
}
func viewDidLoad() {
interactor.fetchUsers { [weak self] users in
DispatchQueue.main.async {
self?.users = users
}
}
}
}

5. View (UserListView)

The View is a SwiftUI component that observes the Presenter’s @Publishedproperties.

import SwiftUI

struct UserListView: View {
@StateObject private var presenter: UserListPresenter
init(presenter: UserListPresenter) {
_presenter = StateObject(wrappedValue: presenter)
}
var body: some View {
NavigationStack {
List(presenter.users) { user in
NavigationLink(value: user) {
Text(user.name)
}
}
.navigationTitle("Users")
.navigationDestination(for: User.self) { user in
UserListRouter.makeUserDetailView(user: user)
}
.onAppear {
presenter.viewDidLoad()
}
}
}
}

6. Detail View (UserDetailView)

This view displays the selected user’s details.

import SwiftUI

struct UserDetailView: View {
let user: User
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("Name: \(user.name)")
.font(.title)
Text("Email: \(user.email)")
.font(.subheadline)
Spacer()
}
.padding()
.navigationTitle("User Details")
}
}

Step 2: Assemble the Components

In the @main entry point, we assemble the VIPER components and launch the app.

import SwiftUI

@main
struct VIPERApp: App {
var body: some Scene {
WindowGroup {
let interactor = UserListInteractor()
let router = UserListRouter()
let presenter = UserListPresenter(interactor: interactor, router: router)
UserListView(presenter: presenter)
}
}
}

Step 3: Run the App

When you run the app, you’ll see a list of users. Selecting a user navigates to the detail screen.

How VIPER Works in SwiftUI

  1. View: The UserListView displays the UI and observes the Presenter using @StateObject.
  2. Presenter: The UserListPresenter fetches data from the Interactor and updates @Published properties.
  3. Interactor: The UserListInteractor contains business logic and fetches data.
  4. Router: The UserListRouter handles navigation by creating views.
  5. Entity: The User struct represents the data model.

Advantages of VIPER with SwiftUI

✅ Declarative UI: SwiftUI simplifies UI development.

✅ Reactive Updates@Published properties automatically refresh views.

✅ Modularity: VIPER’s separation of concerns improves maintainability.

Challenges of VIPER with SwiftUI

⚠️ Boilerplate: VIPER requires additional setup.

⚠️ State Management Conflicts: SwiftUI’s state management may not always align with VIPER’s architecture.

Conclusion

By integrating VIPER with SwiftUI, you can build scalable, maintainable, and testable iOS apps. This approach is ideal for large teams and complex projects where modularity and separation of concerns are critical.

Comments

Popular posts from this blog

Dependency Injection in iOS with SwiftUI

Using Core ML with SwiftUI: Build an AI-Powered App

CI/CD for iOS Projects with Xcode: A Complete Guide