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

Опиши MVVM

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

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

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

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

MVVM (Model-View-ViewModel) архитектура

Определение

MVVM — это архитектурный паттерн, который разделяет приложение на три слоя: Model (данные), View (UI), ViewModel (логика представления). MVVM особенно популярен в iOS благодаря двусторонней привязке данных и хорошей тестируемости.

Компоненты

1. Model — данные и бизнес-логика

// Простая структура данных
struct User: Codable {
    let id: UUID
    let name: String
    let email: String
    let createdAt: Date
}

// С бизнес-логикой
struct User {
    let id: UUID
    let name: String
    let email: String
    
    func isEmailValid() -> Bool {
        return email.contains("@") && email.contains(".")
    }
    
    func isPremium() -> Bool {
        // Бизнес-правило
        return id.uuidString.count > 30
    }
}

2. View — UI, отображение

// SwiftUI View
struct UserProfileView: View {
    @ObservedObject var viewModel: UserProfileViewModel
    
    var body: some View {
        VStack {
            Text(viewModel.userName)
                .font(.title)
            Text(viewModel.userEmail)
                .font(.caption)
            
            if viewModel.isPremium {
                Label("Premium User", systemImage: "star.fill")
            }
        }
        .onAppear {
            viewModel.loadUser()
        }
    }
}

// UIKit View
class UserProfileViewController: UIViewController {
    let viewModel: UserProfileViewModel
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var emailLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
        viewModel.loadUser()
    }
}

3. ViewModel — логика представления

class UserProfileViewModel: ObservableObject {
    // MARK: - Dependencies
    private let userRepository: UserRepository
    private let analyticsService: AnalyticsService
    
    // MARK: - Published properties (двусторонняя привязка)
    @Published var userName: String = ""
    @Published var userEmail: String = ""
    @Published var isPremium: Bool = false
    @Published var isLoading: Bool = false
    @Published var errorMessage: String? = nil
    
    // MARK: - Init (внедрение зависимостей)
    init(userRepository: UserRepository,
         analyticsService: AnalyticsService) {
        self.userRepository = userRepository
        self.analyticsService = analyticsService
    }
    
    // MARK: - Methods
    func loadUser() {
        isLoading = true
        
        Task {
            do {
                let user = try await userRepository.fetchCurrentUser()
                
                // Обновляем published properties
                await MainActor.run {
                    self.userName = user.name
                    self.userEmail = user.email
                    self.isPremium = user.isPremium()
                    self.isLoading = false
                    
                    // Analytics
                    self.analyticsService.logEvent("user_profile_loaded")
                }
            } catch {
                await MainActor.run {
                    self.errorMessage = error.localizedDescription
                    self.isLoading = false
                }
            }
        }
    }
}

Поток данных в MVVM

User Interaction (tap button)
        ↓
View → ViewModel.handleAction()
        ↓
ViewModel обновляет State
        ↓
ViewModel запрашивает данные у Model
        ↓
Model (Repository) делает запрос
        ↓
ViewModel обновляет @Published properties
        ↓
View перерисовывается (SwiftUI) или вызывает обновление (UIKit)
        ↓
UI отображает новое состояние

MVVM vs MVC vs MVP

// ❌ MVC (старый UIKit подход)
class UserViewController: UIViewController, UITableViewDataSource {
    var users: [User] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Логика, сетевые запросы, UI все вместе
        fetchUsers()  // В контроллере
        tableView.dataSource = self
    }
    
    func fetchUsers() {
        // Прямо в контроллере
        URLSession.shared.dataTask(...)
    }
}

// ✅ MVP (Presenter)
class UserViewController: UIViewController {
    let presenter: UserPresenter  // Presenter содержит логику
    
    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.loadUsers()
    }
}

// ✅ MVVM (ViewModel с binding)
class UserViewController: UIViewController {
    let viewModel: UserViewModel
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()  // Привязка к изменениям
        viewModel.loadUsers()
    }
}

Двусторонняя привязка (Binding)

// SwiftUI — встроенная поддержка
struct LoginView: View {
    @ObservedObject var viewModel: LoginViewModel
    
    var body: some View {
        VStack {
            // $email — двусторонняя привязка
            TextField("Email", text: $viewModel.email)
            SecureField("Password", text: $viewModel.password)
            
            Button("Login") {
                viewModel.login()  // View → ViewModel
            }
            
            if let error = viewModel.errorMessage {
                Text(error)  // ViewModel → View
            }
        }
    }
}

class LoginViewModel: ObservableObject {
    @Published var email: String = ""  // View слушает изменения
    @Published var password: String = ""
    @Published var errorMessage: String? = nil
    @Published var isLoading: Bool = false
    
    func login() {
        // email и password уже обновлены из View
        // Отправляем запрос
    }
}

// UIKit — используем Combine
class LoginViewController: UIViewController {
    let viewModel: LoginViewModel
    var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ViewModel → View
        viewModel.$errorMessage
            .assign(to: \.text, on: errorLabel)
            .store(in: &cancellables)
        
        // View → ViewModel
        emailTextField.publisher
            .assign(to: \.email, on: viewModel)
            .store(in: &cancellables)
    }
}

Пример полного MVVM

// 1. Model
struct Post: Identifiable, Decodable {
    let id: Int
    let title: String
    let body: String
}

// 2. Repository (часть Model layer)
protocol PostRepository {
    func fetchPosts() async throws -> [Post]
    func fetchPost(_ id: Int) async throws -> Post
}

// 3. ViewModel
@MainActor
class PostListViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var isLoading = false
    @Published var errorMessage: String? = nil
    @Published var selectedPostId: Int? = nil
    
    private let repository: PostRepository
    
    init(repository: PostRepository) {
        self.repository = repository
    }
    
    func loadPosts() async {
        isLoading = true
        do {
            posts = try await repository.fetchPosts()
            errorMessage = nil
        } catch {
            errorMessage = "Failed to load posts"
            posts = []
        }
        isLoading = false
    }
    
    func selectPost(_ id: Int) {
        selectedPostId = id
    }
}

// 4. View
struct PostListView: View {
    @StateObject var viewModel = PostListViewModel(
        repository: HTTPPostRepository()
    )
    
    var body: some View {
        NavigationStack {
            ZStack {
                if viewModel.isLoading {
                    ProgressView()
                } else if let error = viewModel.errorMessage {
                    Text("Error: \(error)")
                        .foregroundColor(.red)
                } else {
                    List(viewModel.posts) { post in
                        NavigationLink(value: post.id) {
                            PostCell(post: post)
                        }
                        .onTapGesture {
                            viewModel.selectPost(post.id)
                        }
                    }
                }
            }
            .navigationTitle("Posts")
            .onAppear {
                Task {
                    await viewModel.loadPosts()
                }
            }
        }
    }
}

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

Тестируемость — ViewModel можно тестировать отдельно
Отделение логики от UI — легче переиспользовать логику
Двусторонняя привязка — автоматическое обновление UI
Масштабируемость — подходит для больших приложений
Сосредоточенность — каждый слой имеет одну ответственность

Недостатки MVVM

Сложность — больше кода для простых экранов
Кривая обучения — нужно понимать binding
Memory leaks — если неправильно управлять циклами ссылок
Over-engineering — для малых проектов может быть излишним

Когда использовать MVVM

Используй MVVM когда:

  • Приложение с сложной логикой UI
  • Нужны тесты для бизнес-логики
  • Несколько экранов используют одну логику
  • Работаешь с SwiftUI
  • Используешь Combine или RxSwift

Не используй MVVM когда:

  • Простое приложение с несколькими экранами
  • Быстрый прототип
  • Слишком мало ресурсов на архитектуру
Опиши MVVM | PrepBro