Архитектура VIPER в iOS разработке

Введение

VIPER (View, Interactor, Presenter, Entity, Router) — это одна из архитектур, используемых в разработке приложений для iOS. Она направлена на разделение обязанностей и улучшение тестируемости и расширяемости кода. VIPER является эволюцией популярных архитектур MVC (Model-View-Controller) и MVVM (Model-View-ViewModel), добавляя четкое разделение между слоями и строгую модульную структуру.

В этой статье мы подробно рассмотрим, что такое VIPER, какие задачи решают его компоненты, и как они взаимодействуют друг с другом. Мы также приведем примеры кода и визуальное представление структуры компонентов.

Основные компоненты VIPER

VIPER состоит из пяти основных компонентов:

  1. View: Отвечает за отображение данных и взаимодействие с пользователем.
  2. Interactor: Содержит бизнес-логику приложения.
  3. Presenter: Посредник между View и Interactor. Обрабатывает данные, полученные от Interactor, и передает их View.
  4. Entity: Модель данных.
  5. Router: Отвечает за навигацию между экранами.

1. View

Компонент View в VIPER отвечает за отображение данных и взаимодействие с пользователем. Он не содержит бизнес-логики и не управляет навигацией. Взаимодействие с другими компонентами осуществляется через Presenter.

import UIKit

protocol MyViewProtocol: AnyObject {
    func showData(_ data: String)
}

class MyViewController: UIViewController, MyViewProtocol {
    var presenter: MyPresenterProtocol!

    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.viewDidLoad()
    }

    func showData(_ data: String) {
        // Отображение данных в интерфейсе
        print(data)
    }
}

2. Interactor

Interactor содержит бизнес-логику приложения. Он выполняет запросы к базам данных, сетевым сервисам и другим источникам данных. Результаты обработки передаются Presenter для дальнейшего отображения в View.

protocol MyInteractorProtocol: AnyObject {
    func fetchData()
}

class MyInteractor: MyInteractorProtocol {
    var presenter: MyPresenterProtocol!

    func fetchData() {
        // Выполнение бизнес-логики
        let data = "Пример данных"
        presenter.didFetchData(data)
    }
}

3. Presenter

Presenter является посредником между View и Interactor. Он обрабатывает данные, полученные от Interactor, и подготавливает их для отображения в View.

protocol MyPresenterProtocol: AnyObject {
    func viewDidLoad()
    func didFetchData(_ data: String)
}

class MyPresenter: MyPresenterProtocol {
    weak var view: MyViewProtocol?
    var interactor: MyInteractorProtocol!
    var router: MyRouterProtocol!

    func viewDidLoad() {
        interactor.fetchData()
    }

    func didFetchData(_ data: String) {
        view?.showData(data)
    }
}

4. Entity

Entity представляет собой модель данных. Она содержит данные, которые используются в приложении.

struct MyEntity {
    let id: Int
    let name: String
}

5. Router

Router отвечает за навигацию между экранами. Он создаёт и инициализирует компоненты модуля VIPER.

protocol MyRouterProtocol: AnyObject {
    static func createModule() -> UIViewController
}

class MyRouter: MyRouterProtocol {
    static func createModule() -> UIViewController {
        let view = MyViewController()
        let presenter = MyPresenter()
        let interactor = MyInteractor()
        let router = MyRouter()

        view.presenter = presenter
        presenter.view = view
        presenter.interactor = interactor
        presenter.router = router
        interactor.presenter = presenter

        return view
    }
}

Взаимодействие компонентов

Компоненты VIPER взаимодействуют друг с другом через протоколы. Это обеспечивает слабую связность и позволяет легко заменять или модифицировать компоненты.

Жизненный цикл модуля

  1. View вызывает метод viewDidLoad у Presenter.
  2. Presenter инициирует запрос данных у Interactor.
  3. Interactor выполняет бизнес-логику и передает данные обратно Presenter.
  4. Presenter форматирует данные и передает их View для отображения.
  5. Router управляет навигацией и созданием модулей.

Пример приложения на VIPER

Для лучшего понимания рассмотрим пример простого приложения на VIPER. Приложение будет отображать список пользователей и детальную информацию о каждом пользователе.

Структура проекта

  1. Modules
    • UserList
    • UserDetail
  2. Models
    • User

Модель данных

struct User {
    let id: Int
    let name: String
    let email: String
}

Модуль UserList

View

protocol UserListViewProtocol: AnyObject {
    func showUsers(_ users: [User])
}

class UserListViewController: UIViewController, UserListViewProtocol {
    var presenter: UserListPresenterProtocol!

    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.viewDidLoad()
    }

    func showUsers(_ users: [User]) {
        // Отображение списка пользователей
    }
}

Interactor

protocol UserListInteractorProtocol: AnyObject {
    func fetchUsers()
}

class UserListInteractor: UserListInteractorProtocol {
    var presenter: UserListPresenterProtocol!

    func fetchUsers() {
        // Выполнение бизнес-логики для получения списка пользователей
        let users = [User(id: 1, name: "John Doe", email: "john@example.com")]
        presenter.didFetchUsers(users)
    }
}

Presenter

protocol UserListPresenterProtocol: AnyObject {
    func viewDidLoad()
    func didFetchUsers(_ users: [User])
    func showUserDetail(for user: User)
}

class UserListPresenter: UserListPresenterProtocol {
    weak var view: UserListViewProtocol?
    var interactor: UserListInteractorProtocol!
    var router: UserListRouterProtocol!

    func viewDidLoad() {
        interactor.fetchUsers()
    }

    func didFetchUsers(_ users: [User]) {
        view?.showUsers(users)
    }

    func showUserDetail(for user: User) {
        router.navigateToUserDetail(for: user)
    }
}

Router

protocol UserListRouterProtocol: AnyObject {
    static func createModule() -> UIViewController
    func navigateToUserDetail(for user: User)
}

class UserListRouter: UserListRouterProtocol {
    weak var viewController: UIViewController?

    static func createModule() -> UIViewController {
        let view = UserListViewController()
        let presenter = UserListPresenter()
        let interactor = UserListInteractor()
        let router = UserListRouter()

        view.presenter = presenter
        presenter.view = view
        presenter.interactor = interactor
        presenter.router = router
        interactor.presenter = presenter
        router.viewController = view

        return view
    }

    func navigateToUserDetail(for user: User) {
        let userDetailViewController = UserDetailRouter.createModule(with: user)
        viewController?.navigationController?.pushViewController(userDetailViewController, animated: true)
    }
}

Модуль UserDetail

View

protocol UserDetailViewProtocol: AnyObject {
    func showUserDetail(_ user: User)
}

class UserDetailViewController: UIViewController, UserDetailViewProtocol {
    var presenter: UserDetailPresenterProtocol!

    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.viewDidLoad()
    }

    func showUserDetail(_ user: User) {
        // Отображение детальной информации о пользователе
    }
}

Interactor

protocol UserDetailInteractorProtocol: AnyObject {
    func fetchUserDetail()
}

class UserDetailInteractor: UserDetailInteractorProtocol {
    var presenter: UserDetailPresenterProtocol!
    var user: User

    init(user: User) {
        self.user = user
    }

    func fetchUserDetail() {
        // Возвращаем детальную информацию о пользователе
        presenter.didFetchUserDetail(user)
    }
}

Presenter

protocol UserDetailPresenterProtocol: AnyObject {
    func viewDidLoad()
    func didFetchUserDetail(_ user: User)
}

class UserDetailPresenter: UserDetailPresenterProtocol {
    weak var view: UserDetailViewProtocol?
    var interactor: UserDetailInteractorProtocol!
    var router: UserDetailRouterProtocol!

    func viewDidLoad() {
        interactor.fetchUserDetail()
    }

    func didFetchUserDetail(_ user: User) {
        view?.showUserDetail(user)
    }
}

Router

protocol UserDetailRouterProtocol: AnyObject {
    static func createModule(with user: User) -> UIViewController
}

class UserDetailRouter: UserDetailRouterProtocol {
    static func createModule(with user: User) -> UIViewController {
        let view = UserDetailViewController()
        let presenter = UserDetailPresenter()
        let interactor = UserDetailInteractor(user: user)
        let router = UserDetailRouter()

        view.presenter = presenter
        presenter.view = view
        presenter.interactor = interactor
        presenter.router = router
        interactor.presenter = presenter

        return view
    }
}

Визуализация структуры компонентов

Для наглядного представления структуры компонентов VIPER приведем схему взаимодействия между ними:

Заключение

Архитектура VIPER предоставляет четкое разделение обязанностей между компонентами, что делает код более модульным и тестируемым. Внедрение VIPER требует начальных усилий на организацию структуры проекта, но в дальнейшем это окупается улучшенной поддерживаемостью и масштабируемостью кода.

Использование VIPER особенно полезно в крупных проектах с комплексной бизнес-логикой и множеством экранов. Надеюсь, что приведенные примеры и объяснения помогут вам лучше понять, как применять эту архитектуру в своих проектах.