Published on

MVVM amaliyot — to'liq ilova

Authors

Nazariydan amaliyotga. Bu darsda MVVM ni haqiqiy loyiha tuzilmasida qo'llaymiz — fayl joylashuvi, xatolikni boshqarish, yuklanish holatlari va Dependency Injection.

Loyiha fayl tuzilmasi

MeningIlovam/
├── Models/
│   ├── Maqola.swift          ← sof struct
│   └── Foydalanuvchi.swift
├── Views/
│   ├── MaqolalarKorinishi.swift
│   ├── MaqolaTafsiloti.swift
│   └── Components/
│       ├── MaqolaQatori.swift
│       └── YuklanishKorinishi.swift
├── ViewModels/
│   ├── MaqolalarViewModel.swift
│   └── MaqolaTafsilotiViewModel.swift
├── Services/
│   ├── MaqolaXizmati.swift    ← tarmoq so'rovlari
│   └── Protocols/
│       └── MaqolaXizmatiProtocol.swift
└── MeningIlovamApp.swift

To'liq misol — Maqolalar ilovasi

// ═══════════════════════════════════════════════════════════════
//  📦 MODEL — sof ma'lumot tuzilmasi
//
//  Model — "haqiqat manbai". Serverdan kelgan yoki lokal saqlangan
//  ma'lumotning Swift da ifodalanishi.
//
//  Qoidalar:
//  ✅ struct — value type, xavfsiz nusxa
//  ✅ Identifiable — List, ForEach uchun noyob ID
//  ✅ Codable — JSON ↔ Swift avtomatik aylantirish
//  ❌ ViewModel, View, Service haqida bilmaydi
// ═══════════════════════════════════════════════════════════════
struct Maqola: Identifiable, Codable {
    let id: Int           // Server bergan noyob identifikator
    let sarlavha: String  // Maqola nomi
    let tanasi: String    // Maqola to'liq matni
    let muallif: String   // Kim yozgan
    let sana: Date        // Qachon yozilgan
}


// ═══════════════════════════════════════════════════════════════
//  📋 SERVICE PROTOCOL — "shartnoma"
//
//  Protocol = shartnoma. Bu xizmat NIMA QILA OLISHINI belgilaydi,
//  lekin QANDAY qilishini aytmaydi.
//
//  Nima uchun kerak?
//  1. Test da → MockService uzatamiz (tarmoqsiz)
//  2. Preview da → statik ma'lumot uzatamiz
//  3. Haqiqiy ilovada → real API uzatamiz
//
//  Bu "Dependency Inversion Principle" (SOLID dan D)
//  Yuqori qatlam (ViewModel) pastki qatlamga (Service) bog'lanmaydi,
//  ikkalasi ham PROTOKOLGA bog'lanadi
// ═══════════════════════════════════════════════════════════════
protocol MaqolaXizmatiProtocol {
    /// Barcha maqolalarni yuklash — tarmoq yoki lokal
    func maqolalarniYukla() async throws -> [Maqola]

    /// Maqolani o'chirish — ID bo'yicha
    func maqolaniOchir(id: Int) async throws
}


// ═══════════════════════════════════════════════════════════════
//  🌐 HAQIQIY SERVICE — real tarmoq so'rovlari
//
//  Bu class FAQAT tarmoq bilan ishlaydi:
//  - URL yaratish
//  - So'rov yuborish (URLSession)
//  - JSON ni decode qilish (JSONDecoder)
//
//  ViewModel haqida hech narsa bilmaydi!
//  ViewModel uni protocol orqali chaqiradi
// ═══════════════════════════════════════════════════════════════
class MaqolaXizmati: MaqolaXizmatiProtocol {

    func maqolalarniYukla() async throws -> [Maqola] {
        // 1. URL yaratish
        let url = URL(string: "https://api.example.com/maqolalar")!

        // 2. Tarmoq so'rovi — async/await bilan
        //    try — xato bo'lishi mumkin (tarmoq uzilishi)
        //    await — javobni kutish (main thread bloklanmaydi)
        let (data, _) = try await URLSession.shared.data(from: url)

        // 3. JSON → [Maqola] — Codable avtomatik decode
        return try JSONDecoder().decode([Maqola].self, from: data)
    }

    func maqolaniOchir(id: Int) async throws {
        // DELETE so'rov yaratish — RESTful API standarti
        var request = URLRequest(
            url: URL(string: "https://api.example.com/maqolalar/\(id)")!
        )
        request.httpMethod = "DELETE"  // HTTP DELETE metodi
        _ = try await URLSession.shared.data(for: request)
        // Javob kerak emas — faqat muvaffaqiyatni tekshiramiz
    }
}


// ═══════════════════════════════════════════════════════════════
//  🧪 MOCK SERVICE — test va preview uchun SOXTA xizmat
//
//  Mock = soxta. Bu class haqiqiy tarmoq so'rovi YUBORMAYDI.
//  O'rniga — tayyor ma'lumot qaytaradi.
//
//  Qachon kerak?
//  1. Unit test — tarmoqsiz ViewModel ni tekshirish
//  2. Preview — Xcode preview da ma'lumot ko'rsatish
//  3. Offline — internet bo'lmaganda development
//
//  MockService va HaqiqiyService BIR XIL protokolga mos keladi
//  ViewModel farqini bilmaydi — u faqat protokol bilan ishlaydi
// ═══════════════════════════════════════════════════════════════
class MockMaqolaXizmati: MaqolaXizmatiProtocol {
    // Tayyor test ma'lumotlari
    var maqolalar: [Maqola] = [
        Maqola(id: 1, sarlavha: "SwiftUI asoslari",
               tanasi: "SwiftUI haqida...", muallif: "Ali", sana: .now),
        Maqola(id: 2, sarlavha: "MVVM nima?",
               tanasi: "MVVM haqida...", muallif: "Vali", sana: .now),
    ]

    // "Tarmoq so'rovi" — aslida 0.5 soniya kutib tayyor javob
    func maqolalarniYukla() async throws -> [Maqola] {
        // Tarmoq kechikishini simulyatsiya qilish
        // Preview da loading holati ko'rish uchun
        try await Task.sleep(for: .seconds(0.5))
        return maqolalar  // Tayyor ma'lumot qaytarish
    }

    func maqolaniOchir(id: Int) async throws {
        // Lokal massivdan o'chirish — server bo'lmasa ham ishlaydi
        maqolalar.removeAll { $0.id == id }
    }
}


// ═══════════════════════════════════════════════════════════════
//  🧠 VIEWMODEL — barcha logika shu yerda
//
//  @MainActor — barcha @Published o'zgarishlar main thread da
//  bo'lishini kafolatlaydi. Nima uchun kerak?
//  Chunki UI faqat main thread da yangilanadi.
//  Agar background thread dan @Published o'zgartirsangiz — crash!
//
//  ObservableObject — @Published xususiyatlar o'zgarganda
//  barcha kuzatuvchi View larni avtomatik qayta chizadi
// ═══════════════════════════════════════════════════════════════
@MainActor
class MaqolalarViewModel: ObservableObject {

    // ── VIEW HOLATI — ekran qaysi holatda? ──
    // Bu enum View ning UCHTA mumkin bo'lgan holatini ifodalaydi
    // View har holatda BOSHQA UI ko'rsatadi:
    // yuklanmoqda → ProgressView (spinner)
    // yuklandi     → List (maqolalar ro'yxati)
    // xato         → ContentUnavailableView (xato xabari)
    enum ViewHolati {
        case yuklanmoqda       // Tarmoq so'rovi ketdi — kutamiz
        case yuklandi          // Ma'lumot keldi — ko'rsatamiz
        case xato(String)      // Xato bo'ldi — xabar ko'rsatamiz
        // Associated value (String) — xato matni saqlaydi
    }

    // ── @Published xususiyatlar ──
    @Published var maqolalar: [Maqola] = []
    // Serverdan yuklangan maqolalar
    // Bu o'zgarganda → List qayta chiziladi

    @Published var holat: ViewHolati = .yuklanmoqda
    // Joriy holat — View switch bilan tekshiradi
    // Boshlang'ich holat: yuklanmoqda (ilova ochilganda)

    @Published var qidiruvMatni = ""
    // .searchable bilan bog'langan — foydalanuvchi qidirganda

    // ── DEPENDENCY INJECTION ──
    // private — tashqaridan kirish mumkin emas
    // let — o'zgarmas — bir marta beriladi, almashtirilmaydi
    // Turi: MaqolaXizmatiProtocol — ANIQ CLASS EMAS, PROTOKOL!
    // Bu ViewModel ni xizmatdan mustaqil qiladi
    private let xizmat: MaqolaXizmatiProtocol

    // ── INIT — bog'liqlikni TASHQARIDAN qabul qilish ──
    // Default qiymat: MaqolaXizmati() — haqiqiy xizmat
    // Test da: MockMaqolaXizmati() uzatiladi
    // Preview da: MockMaqolaXizmati() uzatiladi
    init(xizmat: MaqolaXizmatiProtocol = MaqolaXizmati()) {
        self.xizmat = xizmat
    }

    // ── COMPUTED PROPERTY — qidiruv natijasi ──
    // View bu property ni ko'rsatadi
    // qidiruvMatni o'zgarganda avtomatik qayta hisoblanadi
    var filtrlangan: [Maqola] {
        guard !qidiruvMatni.isEmpty else { return maqolalar }
        // guard — qidiruv bo'sh bo'lsa hammani ko'rsatish
        return maqolalar.filter {
            $0.sarlavha.localizedCaseInsensitiveContains(qidiruvMatni)
            // localizedCaseInsensitiveContains — katta-kichik harf farq qilmaydi
            // "swift" va "Swift" ikkalasini ham topadi
        }
    }

    // ── YUKLASH — tarmoqdan ma'lumot olish ──
    func yuklash() async {
        holat = .yuklanmoqda  // Spinner ko'rsatish
        do {
            // try — xato bo'lishi mumkin
            // await — natijani kutish (UI bloklanmaydi!)
            maqolalar = try await xizmat.maqolalarniYukla()
            holat = .yuklandi  // Muvaffaqiyat — ro'yxat ko'rsatish
        } catch {
            // Xato bo'ldi — foydalanuvchiga xabar ko'rsatish
            holat = .xato(error.localizedDescription)
        }
    }

    // ── O'CHIRISH — swipe-to-delete uchun ──
    func ochirish(at offsets: IndexSet) async {
        for index in offsets {
            let maqola = filtrlangan[index]
            do {
                // Avval serverdan o'chirish
                try await xizmat.maqolaniOchir(id: maqola.id)
                // Keyin lokal massivdan o'chirish
                maqolalar.removeAll { $0.id == maqola.id }
            } catch {
                holat = .xato("O'chirishda xato: \(error.localizedDescription)")
            }
        }
    }
}


// ═══════════════════════════════════════════════════════════════
//  👁 VIEW — faqat UI ko'rsatish
//
//  View HECH QANDAY logika qilmaydi:
//  ❌ Tarmoq so'rovi yo'q
//  ❌ Ma'lumot formatlash yo'q
//  ❌ Filtrlash logikasi yo'q
//
//  View faqat:
//  ✅ ViewModel dan ma'lumot oladi
//  ✅ ViewModel ga amallar uzatadi
//  ✅ Holat ga qarab boshqa UI ko'rsatadi
// ═══════════════════════════════════════════════════════════════
struct MaqolalarKorinishi: View {
    @StateObject private var viewModel: MaqolalarViewModel

    // Init da xizmat qabul qilish — Preview da mock uzatish uchun
    // _viewModel — @StateObject ning "wrapper" versiyasi
    // StateObject(wrappedValue:) — init ichida yaratish usuli
    init(xizmat: MaqolaXizmatiProtocol = MaqolaXizmati()) {
        _viewModel = StateObject(
            wrappedValue: MaqolalarViewModel(xizmat: xizmat)
        )
    }

    var body: some View {
        NavigationStack {
            Group {
                // switch — holat ga qarab boshqa UI
                // Bu "State-Driven UI" deyiladi
                // ViewModel holat o'zgartiradi → View boshqa narsani ko'rsatadi
                switch viewModel.holat {
                case .yuklanmoqda:
                    // ⏳ Yuklanmoqda — spinner ko'rsatish
                    ProgressView("Yuklanmoqda...")

                case .yuklandi:
                    // ✅ Yuklandi — maqolalar ro'yxati
                    List {
                        ForEach(viewModel.filtrlangan) { maqola in
                            VStack(alignment: .leading) {
                                Text(maqola.sarlavha)
                                    .font(.headline)
                                Text(maqola.muallif)
                                    .font(.caption)
                                    .foregroundStyle(.secondary)
                            }
                        }
                        .onDelete { offsets in
                            // async amal — Task ichida chaqirish
                            Task { await viewModel.ochirish(at: offsets) }
                        }
                    }
                    // .searchable — qidiruv paneli qo'shish
                    .searchable(text: $viewModel.qidiruvMatni,
                                prompt: "Qidirish...")

                case .xato(let xabar):
                    // ❌ Xato — xabar va "Qayta urinish" tugmasi
                    ContentUnavailableView {
                        Label("Xato", systemImage: "exclamationmark.triangle")
                    } description: {
                        Text(xabar)
                    } actions: {
                        Button("Qayta urinish") {
                            Task { await viewModel.yuklash() }
                        }
                    }
                }
            }
            .navigationTitle("Maqolalar")
            // .task — View paydo bo'lganda async amal bajarish
            // Bu onAppear + Task ning qisqartmasi
            .task { await viewModel.yuklash() }
        }
    }
}

// ═══════════════════════════════════════════════════════════════
//  Preview — MOCK xizmat bilan
//  Tarmoq so'rovi yubormaydi! Tayyor ma'lumot ko'rsatadi
//  Preview tez ishlaydi va offline da ham ishlaydi
// ═══════════════════════════════════════════════════════════════
#Preview {
    MaqolalarKorinishi(xizmat: MockMaqolaXizmati())
}

ViewHolati diagrammasi

 ┌───────────┐
 │ yuklanmoqda│ ←── .task { yuklash() }
 └─────┬─────┘
   ┌───┴───┐
   ▼       ▼
┌───────┐ ┌───────┐
│yuklandi│ │ xato  │
└───────┘ └───┬───┘
        "Qayta urinish"
       ┌───────────┐
       │ yuklanmoqda│
       └───────────┘

🎯 Topshiriq: to'liq MVVM ilova

Yuqoridagi misolni Xcode da yarating. MockMaqolaXizmati da 5 ta maqola qo'shing. Qidiruv ishlashini tekshiring. ViewHolati.xato holatini simulyatsiya qiling (mock da throw qo'shing). Preview da mock ishlashini ko'ring. Unit test yozing: ViewModel mock xizmat bilan yuklash() chaqirganda maqolalar to'g'ri yuklanishini tekshiring.

Buy mea coffee