Акции и промокоды Отзывы о школах

MVVM в iOS: избавляемся от хаоса в коде

#Блог

Помните те славные времена, когда все дороги в iOS вели к контроллеру? Massive View Controller – не просто забавное название, а суровая реальность, с которой сталкивался, пожалуй, каждый iOS-разработчик (и если вы говорите, что не сталкивались – я вам не верю).

Но времена меняются, и на смену монолитным контроллерам пришла архитектура MVVM (Model-View-ViewModel) – этакий элегантный способ разделить ответственность между компонентами приложения, не превращая код в неподъемный монолит. И знаете что? Это работает.

Суть MVVM проста, как все гениальное: берем классический MVC от Apple, добавляем к нему ViewModel – прослойку между View и Model – и получаем систему, где каждый занимается своим делом. View отвечает за отображение данных (и только за него!), Model хранит бизнес-логику и данные, а ViewModel… О, это настоящий дирижер нашего оркестра, превращающий сырые данные в то, что можно показать пользователю.

Звучит как очередная серебряная пуля? Не совсем. Но как инструмент для создания чистого, поддерживаемого и тестируемого кода – весьма неплох. Давайте разберемся, почему именно MVVM стал золотым стандартом в мире iOS-разработки.

Преимущества использования MVVM в iOS: почему это не просто модный тренд

Знаете, что общего между хорошей архитектурой приложения и правильно организованной кухней? В обоих случаях каждый должен заниматься своим делом, иначе начнется хаос. И MVVM в этом плане – просто находка (особенно если вы устали от того, что ваш UIViewController похож на сборник всего сущего).

Во-первых, разделение ответственности. Представьте себе классический MVC от Apple как коммунальную квартиру, где контроллер пытается и готовить, и убирать, и счета оплачивать. MVVM же – это отдельные апартаменты для каждого компонента: View занимается только отображением (и слава богу!), Model хранит данные и бизнес-логику, а ViewModel… О, ViewModel – это тот самый идеальный сосед, который берет на себя всю грязную работу по подготовке данных для отображения.

Во-вторых – тестируемость кода. Если вы когда-нибудь пытались написать юнит-тесты для контроллера в MVC, вы знаете, о чем я. Это как пытаться протестировать швейцарский нож – вроде и можно, но как-то неудобно. С MVVM же все логика представления находится в ViewModel, которую можно тестировать отдельно от UI – красота!

И наконец, масштабируемость. Когда ваше приложение растет быстрее, чем количество фреймворков от Apple, MVVM позволяет добавлять новую функциональность без необходимости переписывать половину кодовой базы. Каждый компонент можно расширять независимо, не боясь, что где-то что-то сломается (ну, по крайней мере, теоретически).

На графике представлены четыре критерия, важные для оценки архитектуры мобильного приложения: тестируемость, масштабируемость, читаемость и скорость добавления нового функционала

А еще – и это мой любимый бонус – с MVVM ваш код становится более понятным для новичков в проекте. Они больше не будут смотреть на ваш контроллер как на древний манускрипт, написанный на неизвестном языке. Хотя, возможно, теперь они будут так смотреть на ViewModel – но это уже совсем другая история.

Основные компоненты MVVM: анатомия чистого кода

Давайте разберем MVVM по косточкам – обещаю, будет не так больно, как на уроках анатомии. У нас есть три главных героя этой истории, и каждый из них играет свою роль в этом архитектурном театре.

Model (Модель) – наш хранитель данных и бизнес-логики. Представьте себе его как библиотекаря, который знает все о ваших данных, но понятия не имеет, как они будут отображаться на экране (и слава богу – у него и без этого забот хватает). Модель должна быть максимально независимой от UI – как истинный интроверт, она просто делает свою работу и не заморачивается о том, как ее результаты будут представлены миру.

View (Представление) – это наш фронтмен, звезда сцены, если хотите. В мире iOS это обычно UIView или UIViewController (да-да, тот самый контроллер теперь деградировал до простого view – жизнь полна иронии). View максимально глуп – и это хорошо! Его единственная задача – показывать то, что ему говорят, и сообщать о действиях пользователя. Никакой бизнес-логики, никаких сложных вычислений – просто красивая картинка и обработка тапов.

ViewModel (Модель представления) – а вот это настоящий мозговой центр операции. Если View – это телевизор, то ViewModel – это режиссер, продюсер и сценарист в одном лице. Он берет сырые данные из Model, обрабатывает их до состояния «показать можно», и передает в View. При этом View даже не подозревает о существовании Model – все общение идет через ViewModel, как через высококвалифицированного переводчика.

// Model - наш скромный хранитель данных
struct User {
let id: Int
let name: String
let email: String
}

// ViewModel - наш многозадачный менеджер
class UserViewModel {
private let user: User
   
var displayName: String {
    // Форматируем имя для отображения
    return "👤 " + user.name
}
   
var emailForDisplay: String {
    // Прячем часть email для безопасности
    return email.replacingOccurrences(of: "@", with: " [at] ")
}
}

// View - наш простодушный исполнитель
class UserViewController: UIViewController {
private let viewModel: UserViewModel
   
// Только отображаем данные и слушаем действия пользователя
// Никакой магии, никаких преобразований
}

Важный момент (и тут я надеваю свои очки знатока): каждый компонент должен знать только о том, что ему положено знать. Это как в хорошем детективе – никто не знает всей истории целиком, только свою часть. View знает только о ViewModel, ViewModel знает о Model и View, а Model… Model вообще ни о ком не знает, живет себе спокойно в своем мире чистых данных.

И знаете что? Это работает. Конечно, при условии, что вы не начнете «немножко оптимизировать» и пропускать какие-то связи напрямую. Но об этом мы поговорим в следующих разделах – если вы, конечно, еще не устали от моих метафор.

Реализация MVVM в iOS с использованием UIKit: практика без мистики

Хватит теории – давайте напишем что-нибудь реальное. Представим, что нам нужно создать экран со списком товаров (классика жанра, не правда ли?). И сделаем это так, чтобы потом не было мучительно больно читать этот код через полгода.

Начнем с Model – она у нас будет простой и понятной, как учебник по математике для первого класса:

struct Item {
let id: Int
let name: String
let price: Decimal
let description: String
   
// Никакой UI-логики, только бизнес-правила
var isOnSale: Bool {
    return price < 100
}
}

struct ItemsResponse {
let items: [Item]
let totalCount: Int
let hasMore: Bool
}

Теперь самое интересное – ViewModel. Тут мы развернемся на полную катушку (держитесь крепче, сейчас будет много кода):

class ItemsListViewModel {
// Состояния для View - всё, что может понадобиться для отображения
private(set) var items: [ItemCellViewModel] = []
private(set) var isLoading = false
private(set) var error: Error?
   
// Наш менеджер данных - пусть живёт тут
private let dataManager: DataManager
   
init(dataManager: DataManager) {
    self.dataManager = dataManager
}
   
// Метод загрузки данных - простой и понятный интерфейс для View
func fetchItems(completion: @escaping () -> Void) {
    isLoading = true
 
    dataManager.fetchItems { [weak self] result in
        guard let self = self else { return }
     
        self.isLoading = false
     
        switch result {
        case .success(let response):
            // Преобразуем данные в формат для отображения
            self.items = response.items.map { item in
                ItemCellViewModel(
                    title: item.name,
                    price: self.formatPrice(item.price),
                    isOnSale: item.isOnSale
                )
            }
        case .failure(let error):
            self.error = error
        }
     
        completion()
    }
}
   
private func formatPrice(_ price: Decimal) -> String {
    // Форматирование цены - это явно работа ViewModel
    return "$\(price)"
}
}

// Отдельная ViewModel для ячейки - чтобы жизнь мёдом не казалась
struct ItemCellViewModel {
let title: String
let price: String
let isOnSale: Bool
}

И наконец, View – наш скромный исполнитель чужой воли:

class ItemsListViewController: UIViewController {
private let tableView = UITableView()
private let activityIndicator = UIActivityIndicatorView()
private let viewModel: ItemsListViewModel
   
init(viewModel: ItemsListViewModel) {
    self.viewModel = viewModel
    super.init(nibName: nil, bundle: nil)
}
   
override func viewDidLoad() {
    super.viewDidLoad()
    setupUI()
    loadData()
}
   
private func loadData() {
    viewModel.fetchItems { [weak self] in
        self?.updateUI()
    }
}
   
private func updateUI() {
    activityIndicator.isHidden = !viewModel.isLoading
 
    if let error = viewModel.error {
        // Показываем ошибку - но это тема для отдельного разговора
        print("Ой-ой:", error)
    }
 
    tableView.reloadData()
}
}

extension ItemsListViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return viewModel.items.count
}
   
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    let itemVM = viewModel.items[indexPath.row]
 
    // Настраиваем ячейку, используя данные из ViewModel
    cell.textLabel?.text = itemVM.title
    cell.detailTextLabel?.text = itemVM.price
 
    return cell
}
}

Вот так, шаг за шагом, мы создали приложение, где каждый занимается своим делом: Model хранит данные, ViewModel их обрабатывает, а View просто показывает результат. Красота!

И заметьте – никаких мега-контроллеров, никакой бизнес-логики в UI, всё чисто и понятно. Ну, по крайней мере, понятнее, чем было бы без MVVM (кто помнит времена тысячестрочных контроллеров – поднимите руки!).

Связывание данных между View и ViewModel: когда простота — не порок

Binding данных – та самая магия, которая превращает MVVM из просто красивой идеи в рабочий инструмент. Давайте разберем три способа связать View с ViewModel, начиная от «дедовских» методов и заканчивая современными решениями (спойлер: все они имеют право на жизнь).

  1. Делегаты (Delegation) – старая школа, но работает как часы:
protocol ItemsListViewModelDelegate: AnyObject {
func viewModelDidUpdateItems()
func viewModelDidStartLoading()
func viewModelDidFinishLoading()
}

class ItemsListViewModel {
weak var delegate: ItemsListViewModelDelegate?
   
func fetchItems() {
    delegate?.viewModelDidStartLoading()
 
    dataManager.fetchItems { [weak self] items in
        self?.items = items
        self?.delegate?.viewModelDidUpdateItems()
        self?.delegate?.viewModelDidFinishLoading()
    }
}
}

Да, многовато кода. Да, похоже на бюрократию. Но зато всё явно и понятно – как в документах налоговой (правда, тут хотя бы читать можно).

  1. Замыкания (Closures) – когда хочется чего-то более современного:
class ItemsListViewModel {
var onItemsUpdated: (() -> Void)?
var onLoadingStateChanged: ((Bool) -> Void)?
   
func fetchItems() {
    onLoadingStateChanged?(true)
 
    dataManager.fetchItems { [weak self] items in
        self?.items = items
        self?.onItemsUpdated?()
        self?.onLoadingStateChanged?(false)
    }
}
}

Элегантно, не правда ли? Только не забывайте про weak self – иначе получите утечку памяти в подарок (а их у нас и так хватает).

  1. Реактивное программирование (например, с RxSwift) – для тех, кто любит жить с огоньком:
class ItemsListViewModel {
// Наши Observable-свойства
let items = BehaviorRelay<[Item]>(value: [])
let isLoading = BehaviorRelay(value: false)
   
func fetchItems() {
    isLoading.accept(true)
 
    dataManager.fetchItems()
        .observe(on: MainScheduler.instance)
        .subscribe(onNext: { [weak self] items in
            self?.items.accept(items)
            self?.isLoading.accept(false)
        })
        .disposed(by: disposeBag)
}
}

// А в View всё становится простым и понятным:
class ItemsListViewController: UIViewController {
override func viewDidLoad() {
    super.viewDidLoad()
 
    viewModel.items
        .bind(to: tableView.rx.items(cellIdentifier: "Cell")) { _, item, cell in
            cell.textLabel?.text = item.title
        }
        .disposed(by: disposeBag)
     
    viewModel.isLoading
        .bind(to: activityIndicator.rx.isAnimating)
        .disposed(by: disposeBag)
}
}

Реактивщики среди нас уже наверняка улыбаются – да, это красиво. Но есть нюанс: порог входа высоковат, да и документацию RxSwift придется читать как увлекательный роман (спойлер: он не очень увлекательный).

Какой способ выбрать? Как обычно – зависит от проекта. Если у вас маленькое приложение – делегаты или замыкания будут в самый раз. Для чего-то посерьезнее – реактивный подход может сэкономить кучу времени (после того как вы потратите неделю на его изучение, конечно).

На горизонтальной диаграмме представлено сравнение архитектурных паттернов MVC и MVVM по четырём ключевым критериям: тестируемость, масштабируемость, читаемость и скорость внедрения нового функционала.

И помните главное правило: какой бы способ вы ни выбрали, главное – быть последовательным. Микс из разных подходов в одном проекте – это путь к седым волосам у следующего разработчика, который будет поддерживать ваш код (возможно, этим разработчиком будете вы сами через полгода).

Примеры использования MVVM в реальных приложениях: боевые истории с передовой

Знаете, теория – это прекрасно, но давайте посмотрим, где MVVM действительно спасает жизни разработчиков (ну, или как минимум их нервные клетки). Расскажу о паре реальных кейсов, с которыми я сталкивался – держитесь за кресла, будет интересно.

Случай №1: «Профиль пользователя Instagram на минималках»

Представьте себе экран профиля, где нужно отображать информацию о пользователе и его фотографии. Звучит просто? А теперь добавьте асинхронную загрузку данных с разных эндпоинтов, кеширование, обработку ошибок и состояния загрузки. Уже не так весело, правда?

class ProfileViewModel {
// Композиция - наше всё
private let userViewModel: UserViewModel
private let photosViewModel: PhotosViewModel
   
// Состояния для View
let isLoading = Observable(false)
let error = Observable<Error?>(nil)
   
init(userManager: UserManager, photoManager: PhotoManager) {
    userViewModel = UserViewModel(manager: userManager)
    photosViewModel = PhotosViewModel(manager: photoManager)
 
    // Объединяем состояния загрузки
    combineLatest(userViewModel.isLoading, photosViewModel.isLoading)
        .map { $0 || $1 }
        .bind(to: isLoading)
}
   
func reloadData() {
    // Параллельная загрузка - почему бы и нет?
    userViewModel.fetchUser()
    photosViewModel.fetchPhotos()
}
}

MVVM здесь позволяет разделить сложную логику на управляемые куски, а композиция ViewModel делает код более модульным. Красота!

Случай №2: «Форма заказа из ада»

Сложные формы – это отдельный круг дантова ада для iOS-разработчика. Валидация полей в реальном времени, зависимые поля (когда одно поле влияет на другое), разные форматы данных… Без MVVM тут можно сойти с ума:

class OrderFormViewModel {
// Input
let phoneNumber = Observable("")
let email = Observable("")
let address = Observable("")
   
// Output
let isFormValid = Observable(false)
let phoneNumberError = Observable<String?>(nil)
let emailError = Observable<String?>(nil)
   
init() {
    // Реактивная валидация - это же песня!
    combineLatest(phoneNumber, email, address)
        .map { [weak self] phone, email, address in
            guard let self = self else { return false }
            return self.validatePhone(phone) &&
                  self.validateEmail(email) &&
                  !address.isEmpty
        }
        .bind(to: isFormValid)
}
   
private func validatePhone(_ phone: String) -> Bool {
    // Здесь могла быть ваша регулярка
    return phone.count >= 10
}
}

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

А главное – теперь у нас есть четкое место для каждой части логики. View занимается отображением (и только им!), ViewModel берет на себя всю грязную работу по подготовке данных, а Model… ну, Model просто живет своей спокойной жизнью где-то на бэкенде.

P.S. И да, я знаю, что некоторые скажут «но ведь можно было сделать это и по-другому». Конечно, можно! Но попробуйте сначала поподдерживать такой код годик-другой, а потом поговорим.

Советы по внедрению MVVM в существующие проекты: как не сломать всё и сразу

Знаете, что общего между переходом на MVVM и ремонтом в квартире? В обоих случаях лучше делать это постепенно, если не хотите оказаться в ситуации, когда половина всего сломана, а вторая половина держится на честном слове и изоленте.

Совет №1: Начинайте с нового функционала

Самая распространенная ошибка – попытаться переписать всё и сразу. Не делайте так! Лучше начните с новых фич:

// Новый функционал - красивый и по MVVM
class ShinyNewFeatureViewModel {
// Весь новый код тут
}

// Старый код пока живёт своей жизнью
class OldAndUglyViewController: UIViewController {
// Тут пока всё по-старому
// Но его время придёт...
}

Совет №2: Выделяйте повторяющиеся паттерны

Перед тем как броситься в рефакторинг, посмотрите на свой код критическим взглядом. Скорее всего, вы увидите повторяющиеся паттерны, которые можно вынести в базовые классы:

protocol BaseViewModel {
var isLoading: Observable { get }
var error: Observable<Error?> { get }
}

// Теперь каждая ViewModel может наследоваться от базовой
class UserViewModel: BaseViewModel {
// Уже есть базовый функционал!
}

Совет №3: Обучайте команду (и себя)

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

// Пример для код-ревью: как НЕ надо делать
class BadViewModel {
// О нет, View-логика в ViewModel!
func configureCell(_ cell: UITableViewCell) {
    cell.textLabel?.text = "Так не надо!"
}
}

// Как надо
class GoodViewModel {
// Только данные для отображения
var cellTitle: String {
    return "Вот так правильно!"
}
}

И самое главное – не бойтесь экспериментировать. MVVM достаточно гибкий паттерн, чтобы адаптироваться под ваши нужды. Главное – сохранять основные принципы: разделение ответственности и отсутствие UI-логики в ViewModel.

P.S. И да, держите под рукой валерьянку – она может пригодиться в первые недели перехода. Но потом, обещаю, станет легче!

Заключение: MVVM – не серебряная пуля, но близко к тому

Итак, мы прошли долгий путь от хаоса Massive View Controller до стройной архитектуры MVVM. Чему же мы научились?

Во-первых, MVVM – это не просто модный архитектурный паттерн, а реальный инструмент для создания поддерживаемого кода. Да, возможно, поначалу написание простого экрана займет больше времени, чем при использовании классического MVC. Но поверьте моему опыту – эти инвестиции окупятся сторицей, когда придет время добавлять новую функциональность или искать баги.

Во-вторых, мы увидели, что MVVM – это не догма, а скорее набор хороших практик. Можно использовать делегаты, замыкания или реактивное программирование – главное, чтобы код оставался чистым и понятным. Как говорится, неважно, какого цвета кошка, главное – чтобы она ловила мышей.

И наконец, самое главное – MVVM делает нашу жизнь как разработчиков чуточку лучше. Меньше головной боли при тестировании, меньше конфликтов при слиянии веток, меньше седых волос при поддержке legacy-кода.

Помните: архитектура – это не цель, а средство. И если MVVM помогает вам писать более качественный код – значит, вы на правильном пути.

P.S. А если кто-то скажет вам, что MVVM – это слишком сложно, покажите им свой Massive View Controller годичной давности. Думаю, аргументы закончатся сами собой.

Освоение MVVM требует времени и правильного подхода к обучению. Если вы только начинаете свой путь в iOS-разработке или хотите углубить свои знания в этой области, имеет смысл рассмотреть специализированные курсы. На KursHub собрана актуальная подборка курсов по iOS-разработке, где вы найдете как базовые программы для новичков, так и продвинутые материалы по архитектурным паттернам, включая MVVM. Структурированный подход к обучению поможет быстрее преодолеть крутую кривую обучения и избежать типичных ошибок начинающих MVVM-разработчиков.

Читайте также
рабочий стол
#Блог

Брендбук: как сделать бренд узнаваемым и последовательным

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

Категории курсов
Отзывы о школах