← Назад к вопросам

Можно ли применить VIPER для SwiftUI?

2.0 Middle🔥 191 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Можно ли использовать VIPER со SwiftUI?

Да, архитектуру VIPER можно успешно применять в SwiftUI-приложениях. Несмотря на то, что VIPER изначально проектировалась для UIKit, её принципы — разделение ответственности, однонаправленный поток данных и тестируемость — полностью совместимы с декларативной парадигмой SwiftUI. Однако интеграция требует адаптации, так как SwiftUI активно использует View как состояние, а не как пассивный отображатель.

Ключевые принципы адаптации

Основная задача — переосмыслить роль View в SwiftUI. В классическом VIPER (UIKit) View — это пассивный UIViewController, который получает команды от Presenter. В SwiftUI View — это декларативное описание интерфейса, напрямую связанное с состоянием (@State, @ObservedObject, @StateObject). Поэтому:

  1. SwiftUI View становится комбинацией View и частично Presenter в терминах отображения. Она декларативно описывает UI на основе данных, которые предоставляет Presenter.
  2. Presenter (или ViewModel в адаптации) остается мозгом модуля. Он готовит данные для отображения, обрабатывает пользовательские действия и взаимодействует с Interactor. Он должен предоставлять данные в форме, удобной для SwiftUI, чаще всего как ObservableObject или через биндинги.
  3. Router отвечает за навигацию, используя нативные для SwiftUI инструменты: NavigationStack, sheet, fullScreenCover. Вместо того чтобы держать ссылку на UINavigationController, он может принимать параметры path: Binding<NavigationPath> или вызывать методы, изменяющие состояние навигации.
  4. Interactor и Entity остаются без существенных изменений, так как работают с бизнес-логикой и данными.

Пример реализации модуля VIPER в SwiftUI

Рассмотрим упрощенный модуль отображения списка пользователей.

1. Entity

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

2. Interactor (бизнес-логика)

protocol UsersInteractorProtocol {
    func fetchUsers() async throws -> [User]
}

final class UsersInteractor: UsersInteractorProtocol {
    private let service: NetworkServiceProtocol
    
    init(service: NetworkServiceProtocol = NetworkService()) {
        self.service = service
    }
    
    func fetchUsers() async throws -> [User] {
        // Запрос к API, базе данных и т.д.
        let users: [User] = try await service.request(endpoint: .users)
        return users
    }
}

3. Presenter (подготавливает данные для View)

final class UsersPresenter: ObservableObject {
    @Published var users: [User] = []
    @Published var isLoading = false
    @Published var errorMessage: String?
    
    private let interactor: UsersInteractorProtocol
    private let router: UsersRouterProtocol
    
    init(interactor: UsersInteractorProtocol, router: UsersRouterProtocol) {
        self.interactor = interactor
        self.router = router
    }
    
    @MainActor
    func onAppear() {
        Task {
            isLoading = true
            do {
                users = try await interactor.fetchUsers()
                errorMessage = nil
            } catch {
                errorMessage = "Не удалось загрузить пользователей: \(error.localizedDescription)"
            }
            isLoading = false
        }
    }
    
    func didSelectUser(_ user: User) {
        router.navigateToUserDetails(user)
    }
}

4. Router (навигация)

protocol UsersRouterProtocol {
    func navigateToUserDetails(_ user: User)
}

final class UsersRouter: UsersRouterProtocol {
    private weak var navigationPath: Binding<NavigationPath>?
    
    init(navigationPath: Binding<NavigationPath>?) {
        self.navigationPath = navigationPath
    }
    
    func navigateToUserDetails(_ user: User) {
        navigationPath?.wrappedValue.append(user)
    }
}

5. SwiftUI View

struct UsersView: View {
    @StateObject private var presenter: UsersPresenter
    
    init(presenter: UsersPresenter) {
        _presenter = StateObject(wrappedValue: presenter)
    }
    
    var body: some View {
        NavigationStack {
            List(presenter.users) { user in
                Button(user.name) {
                    presenter.didSelectUser(user)
                }
            }
            .navigationTitle("Пользователи")
            .overlay {
                if presenter.isLoading {
                    ProgressView()
                }
            }
            .alert("Ошибка", 
                    isPresented: .constant(presenter.errorMessage != nil)) {
                Button("OK") { presenter.errorMessage = nil }
            } message: {
                Text(presenter.errorMessage ?? "")
            }
            .onAppear {
                presenter.onAppear()
            }
        }
    }
}

Преимущества и недостатки такого подхода

Преимущества:

  • Сохранение сильных сторон VIPER: четкое разделение слоев, высокая тестируемость (Presenter и Interactor можно тестировать изолированно), масштабируемость для сложных модулей.
  • Совместимость с экосистемой SwiftUI: использование ObservableObject, @Published, нативной навигации.
  • Удобная работа с асинхронностью: Presenter может использовать современные concurrency инструменты (async/await, Task).

Недостатки и сложности:

  • Избыточность для простых экранов: VIPER добавляет много бойлерплейта, что противоречит философии SwiftUI делать простые вещи простыми.
  • Более высокая порог входа: нужно понимать две сложные системы — VIPER и реактивный/декларативный паттерн SwiftUI.
  • Управление зависимостями: требуется тщательная организация инъекции зависимостей между слоями и модулями. Часто необходимо использовать @StateObject или внешнее управление (например, через фабрики).
  • Навигация: реализация Router'а может стать менее прямой из-за декларативной природы навигации в SwiftUI.

Альтернативы и вывод

Для SwiftUI часто рассматривают более легковесные архитектуры, которые лучше соответствуют её реактивной природе:

  • MVVM (Model-View-ViewModel) — наиболее естественная и популярная парадигма для SwiftUI, где ViewModel выступает как ObservableObject.
  • Архитектура на основе состояний (State-Driven) с использованием Reducer и Store (как в TCA — The Composable Architecture).

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

Можно ли применить VIPER для SwiftUI? | PrepBro