Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
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 когда:
- Простое приложение с несколькими экранами
- Быстрый прототип
- Слишком мало ресурсов на архитектуру