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 @Published
properties.
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
- View: The
UserListView
displays the UI and observes the Presenter using@StateObject
. - Presenter: The
UserListPresenter
fetches data from the Interactor and updates@Published
properties. - Interactor: The
UserListInteractor
contains business logic and fetches data. - Router: The
UserListRouter
handles navigation by creating views. - 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
Post a Comment