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

Как реализуешь MVVM на UIKit?

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

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

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

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

Реализация MVVM в UIKit: архитектурный подход

Реализация паттерна Model-View-ViewModel (MVVM) в UIKit требует четкого разделения ответственности между компонентами. Вот подробное описание реализации.

Ключевые компоненты архитектуры

Model – представляет данные и бизнес-логику. Не зависит от UI-слоя. View – UIKit-компоненты (UIViewController, UIView), отвечающие за отображение и пользовательские взаимодействия. ViewModel – посредник между Model и View, преобразует данные модели в формат, пригодный для отображения.

Базовая реализация ViewModel

// MARK: - Model
struct User {
    let id: Int
    let name: String
    let email: String
}

// MARK: - ViewModel
protocol UserViewModelProtocol {
    var userName: String { get }
    var userEmail: String { get }
    var avatarURL: URL? { get }
    
    func loadUserData()
    func updateUserName(_ name: String)
}

class UserViewModel: UserViewModelProtocol {
    // MARK: - Bindings (реактивные свойства)
    var onDataUpdated: (() -> Void)?
    var onErrorOccurred: ((Error) -> Void)?
    
    // MARK: - Private properties
    private var user: User?
    private let userService: UserServiceProtocol
    
    // MARK: - Computed properties (данные для View)
    var userName: String {
        return user?.name ?? "Неизвестный пользователь"
    }
    
    var userEmail: String {
        return user?.email ?? "Нет email"
    }
    
    var avatarURL: URL? {
        guard let userId = user?.id else { return nil }
        return URL(string: "https://api.example.com/avatars/\(userId)")
    }
    
    // MARK: - Initialization
    init(userService: UserServiceProtocol = UserService()) {
        self.userService = userService
    }
    
    // MARK: - Public methods
    func loadUserData() {
        userService.fetchUser { [weak self] result in
            guard let self = self else { return }
            
            switch result {
            case .success(let user):
                self.user = user
                self.onDataUpdated?()
            case .failure(let error):
                self.onErrorOccurred?(error)
            }
        }
    }
    
    func updateUserName(_ name: String) {
        // Валидация и бизнес-логика
        guard !name.isEmpty else {
            onErrorOccurred?(ValidationError.emptyName)
            return
        }
        
        var updatedUser = user
        updatedUser?.name = name
        user = updatedUser
        onDataUpdated?()
    }
}

Реализация View (UIViewController)

// MARK: - ViewController
class UserProfileViewController: UIViewController {
    
    // MARK: - UI Components
    private let nameLabel = UILabel()
    private let emailLabel = UILabel()
    private let avatarImageView = UIImageView()
    private let activityIndicator = UIActivityIndicatorView(style: .large)
    
    // MARK: - Dependencies
    private let viewModel: UserViewModelProtocol
    
    // MARK: - Initialization
    init(viewModel: UserViewModelProtocol) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupBindings()
        loadData()
    }
    
    // MARK: - Setup methods
    private func setupUI() {
        view.backgroundColor = .white
        // Конфигурация UI-компонентов
        nameLabel.font = .systemFont(ofSize: 20, weight: .bold)
        emailLabel.font = .systemFont(ofSize: 16)
        
        // Добавление на view и настройка констрейнтов
        [nameLabel, emailLabel, avatarImageView, activityIndicator].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview($0)
        }
        
        // Констрейнты...
    }
    
    private func setupBindings() {
        // Подписка на обновления ViewModel
        viewModel.onDataUpdated = { [weak self] in
            DispatchQueue.main.async {
                self?.updateUI()
                self?.activityIndicator.stopAnimating()
            }
        }
        
        viewModel.onErrorOccurred = { [weak self] error in
            DispatchQueue.main.async {
                self?.showError(error)
                self?.activityIndicator.stopAnimating()
            }
        }
    }
    
    // MARK: - Data methods
    private func loadData() {
        activityIndicator.startAnimating()
        viewModel.loadUserData()
    }
    
    private func updateUI() {
        nameLabel.text = viewModel.userName
        emailLabel.text = viewModel.userEmail
        
        if let avatarURL = viewModel.avatarURL {
            loadAvatar(from: avatarURL)
        }
    }
    
    private func loadAvatar(from url: URL) {
        // Загрузка изображения (можно вынести в отдельный сервис)
        URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
            guard let data = data, let image = UIImage(data: data) else { return }
            DispatchQueue.main.async {
                self?.avatarImageView.image = image
            }
        }.resume()
    }
}

Способы связывания View и ViewModel

1. Closure-based binding (как в примере выше)

viewModel.onDataUpdated = { [weak self] in
    self?.updateUI()
}

2. Property Observer

class UserViewModel {
    var user: User? {
        didSet {
            onDataUpdated?()
        }
    }
}

3. Combine Framework (iOS 13+)

import Combine

class UserViewModel {
    @Published var userName: String = ""
    private var cancellables = Set<AnyCancellable>()
}

// В ViewController
viewModel.$userName
    .receive(on: DispatchQueue.main)
    .sink { [weak self] name in
        self?.nameLabel.text = name
    }
    .store(in: &cancellables)

4. RxSwift/RxCocoa

// ViewModel
let userNameSubject = BehaviorSubject<String>(value: "")

// ViewController
viewModel.userNameSubject
    .bind(to: nameLabel.rx.text)
    .disposed(by: disposeBag)

Преимущества MVVM в UIKit

  • Тестируемость: ViewModel не зависит от UIKit, можно тестировать без UI
  • Разделение ответственности: четкие границы между слоями
  • Поддержка реактивного программирования: удобные binding-механизмы
  • Упрощение ViewController: перенос логики в ViewModel уменьшает "массивный ViewController"
  • Повторное использование: ViewModel можно использовать с разными View

Рекомендации по реализации

  1. Используйте протоколы для ViewModel для улучшения тестируемости
  2. Инжектируйте зависимости через инициализатор
  3. Обрабатывайте потоки данных через главную очередь (DispatchQueue.main)
  4. Избегайте сильных ссылок на View в ViewModel
  5. Выносите сетевые запросы и работу с данными в отдельные сервисы
  6. Используйте DI-контейнеры (Swinject, Needle) для управления зависимостями

Пример сервисного слоя

protocol UserServiceProtocol {
    func fetchUser(completion: @escaping (Result<User, Error>) -> Void)
}

class UserService: UserServiceProtocol {
    func fetchUser(completion: @escaping (Result<User, Error>) -> Void) {
        // Сетевая логика или работа с CoreData
        let user = User(id: 1, name: "Иван Иванов", email: "ivan@example.com")
        completion(.success(user))
    }
}

Такой подход создает чистую, поддерживаемую архитектуру, которая масштабируется по мере роста приложения и упрощает тестирование всех компонентов.

Как реализуешь MVVM на UIKit? | PrepBro