Как вы применяли паттерн MVVM в iOS-разработке?

Ответ

MVVM (Model-View-ViewModel) — это архитектурный паттерн, который разделяет ответственность, упрощает тестирование и уменьшает связность кода.

Структура компонентов:

  • Model: Представляет данные и бизнес-логику (например, структуры User, Post).
  • View: Отвечает за отображение UI и пользовательские взаимодействия (UIViewController, UIView, SwiftUI View). View содержит ссылку на ViewModel, но не наоборот.
  • ViewModel: Промежуточный слой, который преобразует данные Model в удобный для View формат. Он не должен импортировать UIKit/SwiftUI.

Пример реализации с биндингом через замыкания:

// Model
struct User {
    let name: String
    let email: String
}

// ViewModel
class UserViewModel {
    private let user: User

    // Published свойства для биндинга
    var userName: String { user.name }
    var userEmail: String { user.email }

    // Замыкание для обновления View
    var onUpdate: (() -> Void)?

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

    func fetchUserData() {
        // Сетевой запрос или работа с БД
        // После получения данных вызываем onUpdate?()
    }
}

// View (UIViewController)
class UserViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var emailLabel: UILabel!

    var viewModel: UserViewModel! {
        didSet {
            viewModel.onUpdate = { [weak self] in
                self?.updateUI()
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        updateUI()
        viewModel.fetchUserData()
    }

    private func updateUI() {
        nameLabel.text = viewModel.userName
        emailLabel.text = viewModel.userEmail
    }
}

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

  • Тестируемость: ViewModel легко тестируется юнит-тестами без UI.
  • Разделение ответственности: View занимается только отрисовкой, ViewModel — подготовкой данных.
  • Гибкость: Для биндинга можно использовать Combine, RxSwift или простые замыкания.

Важно: ViewModel не должна хранить сильные ссылки на View, чтобы избежать циклов удержания.

Ответ 18+ 🔞

Слушай, а вот этот ваш MVVM, ну, архитектурный паттерн такой, блядь. Не просто так его придумали, а чтобы мозги не ебать и код не превращал в спагетти из ада, понимаешь?

Короче, смотри, как оно работает, без этой вашей заумщины.

Кто есть кто в этой пиздопроебибне:

  • Model (Модель): Это, типа, голые данные и правила игры. User, Post там всякие. Тупая структура, которая сама по себе, блядь. Ничего не знает ни о каком интерфейсе, живёт в своём мире.
  • View (Вьюха): Это уже то, что ты видишь на экране. UIViewController, SwiftUI View. Её задача — рисовать кнопочки и реагировать на тапы. Она, дура, должна быть максимально тупой. Единственное, что она знает — это её ViewModel.
  • ViewModel (ВьюМодель): А вот это, сука, самый главный мудак на районе! Промежуточный слой, переводчик с языка моделей на язык вьюх. Он берёт сырые данные из User и делает из них "Имя: Вася Пупкин", чтобы вьюхе было удобно. И главное правило, блядь, которое нарушают все распиздяи — ViewModel НЕ ДОЛЖЕН ИМПОРТИРОВАТЬ UIKit или SwiftUI. Вообще. Никак. Он должен быть чистым, как слеза младенца, чтобы его можно было тестировать в полной изоляции.

Вот тебе пример, как это выглядит в коде, на замыканиях, чтобы не париться с Combine пока:

// Model (Модель) - просто данные, ёпта
struct User {
    let name: String
    let email: String
}

// ViewModel (ВьюМодель) - тут вся магия
class UserViewModel {
    private let user: User // Держит модельку

    // Вот эти свойства уже для вьюхи, отформатированные
    var userName: String { user.name }
    var userEmail: String { user.email }

    // Замыкание-сигналка для вьюхи: "Эй, дура, обновляйся!"
    var onUpdate: (() -> Void)?

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

    func fetchUserData() {
        // Тут типа идёт в сеть или в базу, ебаться с API
        // Получил данные — кричи вьюхе: onUpdate?()
        // Это типа триггер для обновления
    }
}

// View (Вьюха, UIViewController) - тупая, но красивая
class UserViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var emailLabel: UILabel!

    // Держит свою ViewModel!
    var viewModel: UserViewModel! {
        didSet {
            // Подписывается на её сигналку: "Как позовёшь — обновлюсь!"
            viewModel.onUpdate = { [weak self] in
                self?.updateUI()
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        updateUI() // Первоначальная настройка
        viewModel.fetchUserData() // Говорим VM: "Давай, работай!"
    }

    private func updateUI() {
        // Просто берёт готовые данные из VM и тыкает в лейблы
        nameLabel.text = viewModel.userName
        emailLabel.text = viewModel.userEmail
    }
}

И в чём, блядь, кайф-то?

  • Тестируемость — овердохуищная: ViewModel можно тестировать отдельно, как боженьку. Подсунул ей мок-данные и проверяй, что она правильно их готовит. Без всяких симуляторов и интерфейсов, в рот меня чих-пых!
  • Разделение, ёпта: Вьюха не лезет в бизнес-логику, ViewModel не лезет в отрисовку. Каждый сосёт свою сосульку.
  • Гибкость: Не нравятся замыкания? Бери Combine, RxSwift — суть не меняется, только способ подписки.

И главное предупреждение, на котором все обжигаются: ViewModel НЕ ДОЛЖНА держать сильную ссылку на View. Только слабую или через замыкание с [weak self], иначе получишь цикл удержания, и твое приложение накроется медным тазом, а память утечёт, как вода в песок. Запомни это, как "Отче наш".