Azimjon Abdurasulov

02. MVVM — Data Binding va Reaktiv Holat

Arxitektura

iOS Arxitektura 2: MVVM — Model-View-ViewModel, Data Binding va Zamonaviy iOS Dasturlash

Holat: 2026-yil oktabr. MVVM iOS dasturlashda eng ko'p qo'llaniladigan, ayniqsa SwiftUI va Combine texnologiyalari bilan birga eng yaxshi moslashadigan va sanoat standartiga aylangan arxitektura hisoblanadi.

Mundarija

  1. MVVM nima?
  2. ViewModel qoidalari
  3. Data Binding usullari (iOS 2024-2026)
  4. Amaliy loyiha: User List MVVM (Combine binding)
  5. Amaliy loyiha 2: Xuddi shu app SwiftUI + MVVM
  6. MVVM + Input/Output pattern
  7. Unit Testing ViewModel
  8. MVVM kamchiliklari
  9. MVVM vs MVC taqqoslash jadvali
  10. Xulosa
  11. Manbalar

1. MVVM nima? {#mvvm-nima}

MVVM (Model-View-ViewModel) — bu dasturning mantiqiy (biznes) qismini va foydalanuvchi interfeysini (UI) bir-biridan ajratishga yordam beruvchi arxitektura paternidir. MVC'dan farqli ravishda, MVVM UI va biznes logikani butunlay mustaqil qiladi.

  • Model: Ma'lumotlar tuzilmasi (struct'lar, Core Data entity'lari, networking modellari).
  • View: UI (UIViewController va uning ichidagi UIView'lar YOKI SwiftUI View). Foydalanuvchi harakatlarini qabul qiladi va ViewModel'ga uzatadi.
  • ViewModel: View uchun ma'lumotlarni tayyorlaydigan qatlam. Model'dan ma'lumotni oladi, uni View tushunadigan ko'rinishga (String, Bool va h.k.) o'zgartiradi.

MVVM Arxitekturasi va Ma'lumotlar Oqimi

Muhim: View ViewModel'ni biladi va unga bog'langan (Reference bor), lekin ViewModel View'ni umuman BILMAYDI! ViewModel'da UI elementlari haqida hech qanday tasavvur yo'q.

2. ViewModel qoidalari {#viewmodel-qoidalari}

Haqiqiy MVVM'ni yashiringan MVC'dan ajratib turadigan qat'iy qoidalar mavjud:

  1. ViewModel UIKit import QILMASLIGI kerak: ViewModel faylida import UIKit degan yozuv bo'lishi mumkin emas (Foundation yoki Combine etarli).
  2. ViewModel faqat biznes logika va data transformation bajaradi: Modelni olib, uni View ko'rsatishi mumkin bo'lgan holatga keltiradi.
  3. ViewModel'da UIColor, UIFont, UIImage yo'q: Faqat oddiy turlar (String, Bool, Int, enum) ishlatilishi kerak. Masalan, rangni ko'rsatish uchun ViewModel String (hex) yoki Enum qaytarishi kerak.

Senior eslatmasi: Agar ViewModel'ingiz ichida UIView yoki uning vorislariga reference bo'lsa, tabriklayman — siz MVVM emas, qo'rqinchli "Massive MVC" yozyapsiz!

3. Data Binding usullari (iOS 2024-2026) {#data-binding}

View va ViewModel'ni bir-biriga bog'lash (Data Binding) uchun zamonaviy iOS'da bir nechta usullar mavjud:

Data Binding Mexanizmlari

a) @Observable (iOS 17+, ENG ZAMONAVIY)

Swift 5.9 bilan kelgan yangi makro orqali qilinadi. SwiftUI uchun eng toza usul, UIKit bilan ham manual ishlash mumkin.

import Foundation
import Observation

@Observable
final class UserViewModel {
    var name = ""
    var isLoading = false
}

b) Combine (iOS 13+)

Reaktiv dasturlash orqali Publisher va Subscriber yordamida ishlash.

import Foundation
import Combine

final class CombineViewModel {
    @Published var users: [String] = []
    
    // View tomonida:
    // viewModel.$users.sink { [weak self] users in self?.updateUI() }
}

c) Closures (eng oddiy, framework kerak emas)

Hech qanday qo'shimcha frameworklarsiz callback'lar orqali bog'lash.

final class ClosureViewModel {
    var onUsersUpdated: (([String]) -> Void)?
    var users: [String] = [] {
        didSet { onUsersUpdated?(users) }
    }
}

d) async/await (Swift Concurrency)

Asinxron kodlar uchun eng yaxshi usul.

final class AsyncViewModel {
    func fetchUsers() async throws -> [String] {
        // Tarmoq so'rovi
        return ["Ali", "Vali"]
    }
    
    // View tomonida: Task { let users = try await viewModel.fetchUsers() }
}

4. Amaliy loyiha: User List MVVM (Combine binding) {#loyihauikit}

Quyida UIKit, SnapKit va Combine orqali qilingan to'liq ishlaydigan MVVM misoli:

import UIKit
import Combine
import SnapKit

// 1. Model
struct User: Identifiable, Codable {
    let id: Int
    let name: String
    let email: String
}

// 2. Service
protocol UserServiceProtocol {
    func fetchUsers() async throws -> [User]
}

final class UserService: UserServiceProtocol {
    func fetchUsers() async throws -> [User] {
        let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([User].self, from: data)
    }
}

// 3. ViewModel
@MainActor
final class UserListViewModel {
    @Published private(set) var users: [User] = []
    @Published private(set) var isLoading: Bool = false
    @Published private(set) var error: String?
    
    private let userService: UserServiceProtocol
    
    init(userService: UserServiceProtocol = UserService()) {
        self.userService = userService
    }
    
    func loadUsers() {
        isLoading = true
        error = nil
        
        Task {
            do {
                self.users = try await userService.fetchUsers()
            } catch {
                self.error = error.localizedDescription
            }
            isLoading = false
        }
    }
}

// 4. ViewController
final class UserListViewController: UIViewController {
    private let viewModel = UserListViewModel()
    private var cancellables = Set<AnyCancellable>()
    
    private let tableView = UITableView()
    private let activityIndicator = UIActivityIndicatorView(style: .large)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        bindViewModel()
        viewModel.loadUsers()
    }
    
    private func setupUI() {
        title = "Foydalanuvchilar"
        view.backgroundColor = .systemBackground
        
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        tableView.dataSource = self
        
        view.addSubview(tableView)
        view.addSubview(activityIndicator)
        
        tableView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        activityIndicator.snp.makeConstraints { make in
            make.center.equalToSuperview()
        }
    }
    
    private func bindViewModel() {
        viewModel.$users
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.tableView.reloadData()
            }
            .store(in: &cancellables)
        
        viewModel.$isLoading
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isLoading in
                isLoading ? self?.activityIndicator.startAnimating() : self?.activityIndicator.stopAnimating()
            }
            .store(in: &cancellables)
            
        viewModel.$error
            .compactMap { $0 }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] errorMessage in
                let alert = UIAlertController(title: "Xato", message: errorMessage, preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "OK", style: .default))
                self?.present(alert, animated: true)
            }
            .store(in: &cancellables)
    }
}

extension UserListViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.users.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let user = viewModel.users[indexPath.row]
        var content = cell.defaultContentConfiguration()
        content.text = user.name
        content.secondaryText = user.email
        cell.contentConfiguration = content
        return cell
    }
}

5. Amaliy loyiha 2: Xuddi shu app SwiftUI + MVVM {#loyihaswiftui}

Eng qizig'i shundaki, to'g'ri yozilgan UserListViewModel nafaqat UIKit'da, balki SwiftUI'da ham hech qanday o'zgarishsiz ishlaydi! (Faqat u ObservableObject bo'lishi kerak, shuning uchun tepadagi kodga ObservableObject qoshilgan deb faraz qilamiz).

import SwiftUI

// ViewModel SwiftUI uchun ham huddi shunday ishlaydi
// class UserListViewModel: ObservableObject ...

struct UserListView: View {
    @StateObject private var viewModel = UserListViewModel()
    
    var body: some View {
        NavigationView {
            Group {
                if viewModel.isLoading {
                    ProgressView("Yuklanmoqda...")
                } else {
                    List(viewModel.users) { user in
                        VStack(alignment: .leading) {
                            Text(user.name)
                                .font(.headline)
                            Text(user.email)
                                .font(.subheadline)
                                .foregroundColor(.secondary)
                        }
                    }
                }
            }
            .navigationTitle("Foydalanuvchilar")
            .onAppear {
                viewModel.loadUsers()
            }
            .alert("Xato", isPresented: .constant(viewModel.error != nil)) {
                Button("OK") { /* xatoni tozalash mantiqi */ }
            } message: {
                Text(viewModel.error ?? "")
            }
        }
    }
}

6. MVVM + Input/Output pattern {#input-output}

Katta loyihalarda ViewModel interfeysini toza saqlash uchun Input va Output protokollari qo'llaniladi.

import Combine

protocol UserListViewModelInput {
    func viewDidLoad()
    func didSelectUser(at index: Int)
}

protocol UserListViewModelOutput {
    var users: AnyPublisher<[User], Never> { get }
    var isLoading: AnyPublisher<Bool, Never> { get }
}

protocol UserListViewModelType {
    var input: UserListViewModelInput { get }
    var output: UserListViewModelOutput { get }
}

// Amalga oshirish:
final class UserListViewModelIO: UserListViewModelType, UserListViewModelInput, UserListViewModelOutput {
    var input: UserListViewModelInput { self }
    var output: UserListViewModelOutput { self }
    
    // Outputs
    private let usersSubject = CurrentValueSubject<[User], Never>([])
    var users: AnyPublisher<[User], Never> { usersSubject.eraseToAnyPublisher() }
    
    private let loadingSubject = CurrentValueSubject<Bool, Never>(false)
    var isLoading: AnyPublisher<Bool, Never> { loadingSubject.eraseToAnyPublisher() }
    
    // Inputs
    func viewDidLoad() {
        loadingSubject.send(true)
        // fetch...
    }
    
    func didSelectUser(at index: Int) {
        let selected = usersSubject.value[index]
        print("Selected: \(selected.name)")
    }
}

Bu yondashuv kod o'qilishini ancha osonlashtiradi va test yozishga juda qulay.

7. Unit Testing ViewModel {#unit-testing}

MVVM'ning eng katta yutuqlaridan biri — UI bo'lmagani uchun uni test qilish juda oson.

import XCTest
import Combine
@testable import MyApp

// 1. Mock Service yaratamiz
final class MockUserService: UserServiceProtocol {
    var shouldFail = false
    
    func fetchUsers() async throws -> [User] {
        if shouldFail {
            throw URLError(.badServerResponse)
        }
        return [User(id: 1, name: "Test User", email: "test@test.com")]
    }
}

// 2. Test yozamiz
@MainActor
final class UserListViewModelTests: XCTestCase {
    var sut: UserListViewModel! // System Under Test
    var mockService: MockUserService!
    
    override func setUp() {
        super.setUp()
        mockService = MockUserService()
        sut = UserListViewModel(userService: mockService)
    }
    
    func test_loadUsers_success_updatesUsers() async {
        // Arrange
        mockService.shouldFail = false
        
        // Act
        sut.loadUsers()
        
        // async method tugashini biroz kutamiz
        try? await Task.sleep(nanoseconds: 100_000_000)
        
        // Assert
        XCTAssertFalse(sut.isLoading)
        XCTAssertEqual(sut.users.count, 1)
        XCTAssertEqual(sut.users.first?.name, "Test User")
        XCTAssertNil(sut.error)
    }
}

8. MVVM kamchiliklari {#kamchiliklar}

Har qanday arxitektura singari MVVM ham mukammal emas:

  1. Kichik loyihalarda ortiqcha murakkablik: Bitta tugma va bitta text bo'lgan ekranga MVVM yozish — ortiqcha vaqt sarfi.
  2. Binding boilerplate: Combine yoki tayyor framework'larsiz closure/KVO orqali binding qilish juda ko'p qaytariluvchi kod (boilerplate) yozishni talab qiladi.
  3. Massive ViewModel xavfi: Agar ehtiyot bo'lmasangiz, Massive View Controller muammosi endi "Massive ViewModel" muammosiga aylanadi.
  4. Navigatsiya: MVVM'ning o'zida ekrandan-ekranga o'tish (routing) uchun aniq qoida yo'q (Shuning uchun Coordinator pattern bilan birga ishlatiladi).

9. MVVM vs MVC taqqoslash jadvali {#taqqoslash}

XususiyatMVCMVVM
Rol ajratishO'rtacha (Controller juda ko'p ish qiladi)Zo'r (Logika to'liq ajratilgan)
Test qilish qulayligiQiyin (UI bilan aralashgan)Juda oson (ViewModel'da UI yo'q)
BindingYo'q (Manual update qilinadi)Bor (Avtomatik yangilanish)
O'rganish tezligiJuda osonO'rtacha (Binding'ni o'rganish kerak)
SwiftUI mosligiYomon moslashadiTabiiy moslashgan

Xulosa {#xulosa}

MVVM arxitekturasi zamonaviy iOS dasturlashning "oltin standarti" hisoblanadi. U kodingizni toza, o'qilishi oson va eng muhimi — test qilinadigan holatga keltiradi. ViewModel'da UIKitni qat'iy cheklash va to'g'ri Data Binding texnikasini tanlash muvaffaqiyatli MVVM loyihaning asosiy kalitidir.

Manbalar {#manbalar}

Markdown manba

iOS Lessons