С каким стеком технологий в iOS используется архитектура MVVM?

«С каким стеком технологий в iOS используется архитектура MVVM?» — вопрос из категории Архитектура, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

MVVM (Model-View-ViewModel) в iOS применяется со следующими технологическими стеками, обеспечивающими реактивную связь между ViewModel и View:

  • SwiftUI — нативный, предпочтительный выбор. Встроенные property wrappers (@StateObject, @ObservedObject, @Published) напрямую реализуют реактивные обновления.
  • UIKit + Combine — стандартный стек для реактивного программирования в UIKit. ViewModel реализует ObservableObject, а View подписывается на его @Published свойства.
  • UIKit + RxSwift/ReactiveSwift — альтернативы Combine, часто встречаются в legacy-проектах.

Почему важен реактивный стек? ViewModel не должна напрямую ссылаться на View. Вместо этого View подписывается на изменения состояния ViewModel, что обеспечивает слабую связность и упрощает тестирование.

Пример с UIKit и Combine:

// ViewModel
class UserListViewModel: ObservableObject {
    @Published var users: [User] = []
    private var cancellables = Set<AnyCancellable>()

    func loadUsers() {
        NetworkService.fetchUsers()
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { _ in },
                  receiveValue: { [weak self] users in
                      self?.users = users
                  })
            .store(in: &cancellables)
    }
}

// View (UIViewController)
class UserListViewController: UIViewController {
    private let viewModel = UserListViewModel()
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupBindings()
        viewModel.loadUsers()
    }

    private func setupBindings() {
        viewModel.$users
            .receive(on: DispatchQueue.main)
            .sink { [weak self] users in
                self?.tableView.reloadData()
            }
            .store(in: &cancellables)
    }
}