- Published on
MVVM amaliyot — to'liq ilova
- Authors
- Name
- ShoxruxC
- @iOSdasturchi
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.