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

Как можно отделить Controller от View?

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

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

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

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

Разделение Controller и View в iOS-разработке

Отделение Controller от View — фундаментальный принцип проектирования iOS-приложений, который повышает тестируемость, переиспользуемость и поддерживаемость кода. Вот основные подходы и паттерны:

1. Использование MVVM (Model-View-ViewModel)

MVVM — наиболее популярный современный подход, где ViewModel выступает посредником, беря на себя бизнес-логику и подготовку данных для отображения.

// ViewModel
class UserProfileViewModel {
    private let userService: UserServiceProtocol
    @Published var userName: String = ""
    @Published var isLoading: Bool = false
    
    init(userService: UserServiceProtocol) {
        self.userService = userService
    }
    
    func loadUserData() {
        isLoading = true
        userService.fetchUser { [weak self] user in
            self?.userName = user.name
            self?.isLoading = false
        }
    }
}

// ViewController (теперь только управление View)
class UserProfileViewController: UIViewController {
    private let viewModel: UserProfileViewModel
    private var cancellables = Set<AnyCancellable>()
    
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
        viewModel.loadUserData()
    }
    
    private func bindViewModel() {
        viewModel.$userName
            .assign(to: \.text, on: nameLabel)
            .store(in: &cancellables)
        
        viewModel.$isLoading
            .map { !$0 }
            .assign(to: \.isHidden, on: activityIndicator)
            .store(in: &cancellables)
    }
}

2. Выделение кастомных View-компонентов

Создание переиспользуемых UIView-подклассов с собственной логикой отрисовки:

// Кастомная View
class BadgeView: UIView {
    private let label = UILabel()
    var count: Int = 0 {
        didSet {
            label.text = "\(count)"
            updateAppearance()
        }
    }
    
    private func updateAppearance() {
        backgroundColor = count > 0 ? .red : .gray
        isHidden = count == 0
    }
    
    // Вся логика отрисовки инкапсулирована здесь
}

3. Использование Presenter (VIPER, MVP)

В VIPER и MVP презентер содержит всю бизнес-логику, а View (ViewController) становится пассивным:

// Presenter в MVP
protocol UserViewProtocol: AnyObject {
    func displayUserName(_ name: String)
    func showLoading(_ isLoading: Bool)
}

class UserPresenter {
    weak var view: UserViewProtocol?
    private let userService: UserServiceProtocol
    
    func loadUser() {
        view?.showLoading(true)
        userService.fetchUser { [weak self] user in
            self?.view?.showLoading(false)
            self?.view?.displayUserName(user.name)
        }
    }
}

4. Отделение DataSource и Delegate

Вынос логики таблиц и коллекций в отдельные классы:

class UserDataSource: NSObject, UITableViewDataSource {
    private var users: [User] = []
    
    func updateUsers(_ users: [User]) {
        self.users = users
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return users.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath)
        let user = users[indexPath.row]
        cell.textLabel?.text = user.name
        return cell
    }
}

// В ViewController остается только:
dataSource.updateUsers(users)
tableView.reloadData()

5. Использование Router/Coordinator для навигации

Coordinator паттерн полностью отделяет логику навигации:

protocol UserCoordinatorProtocol {
    func showUserDetails(for user: User)
    func showSettings()
}

class UserCoordinator: UserCoordinatorProtocol {
    private let navigationController: UINavigationController
    
    func showUserDetails(for user: User) {
        let detailsVC = UserDetailsViewController(user: user)
        navigationController.pushViewController(detailsVC, animated: true)
    }
}

6. Применение реактивного программирования

Использование Combine или RxSwift для декларативного связывания данных:

// ViewModel предоставляет Publisher'ы
// ViewController подписывается на изменения
viewModel.$userProfile
    .receive(on: DispatchQueue.main)
    .sink { [weak self] profile in
        self?.updateUI(with: profile)
    }
    .store(in: &cancellables)

Ключевые преимущества разделения:

  • Тестируемость: ViewModel/Presenter можно тестировать без UIKit
  • Переиспользуемость: View-компоненты работают с разными Controller'ами
  • Поддерживаемость: Четкое разделение ответственности (Single Responsibility)
  • Гибкость: Легкая замена слоев представления

Практические рекомендации:

  1. Строгое разделение: View не должна знать о Model, Controller не должен заниматься форматированием данных
  2. Инъекция зависимостей: Все зависимости передаются через инициализаторы
  3. Протокол-ориентированный дизайн: Использование протоколов для уменьшения связности
  4. Логика вьюхи во View: Анимации, отрисовка, констрейнты — только во View-слое

Эти подходы позволяют создавать архитектуру, где ViewController становится тонким координатором, а основная логика распределяется между специализированными компонентами, что соответствует принципам чистой архитектуры и SOLID.