Published on

SwiftUI-da EnvironmentObject bilan ViewModel qo'shish

Authors

ViewModel nima va nima uchun kerak

MVVMdagi VM — bu ViewModel, va u arxitekturaning asosiy qismi hisoblanadi. Bir tomonda View (barcha UI komponentlar), ikkinchi tomonda Model (ma'lumot nuqtalari), va o'rtada esa ViewModel turadi.

ViewModel view-ga bog'langan bo'ladi va view uchun zarur barcha ma'lumotlarni saqlaydi — bu ma'lumotlar, albatta, oldingi videoda yaratgan ItemModellarimiz. Shuningdek, ViewModel ma'lumotlarni yaratish, o'qish, yangilash va o'chirish bo'yicha barcha mantiqni (logic) o'z ichiga oladi.


ListView-ga funksiyalar qo'shish (ViewModel oldidan)

Oldin, ViewModel yaratishdan avval, funksiyalarni to'g'ridan-to'g'ri ListViewga qanday qo'shish mumkinligini ko'rsatib o'tamiz — keyinroq ularni ViewModel-ga ko'chiramiz.

Swiping to'g'risida o'chirish (delete)

ForEach loop-ining pastiga .onDelete qo'shamiz:

.onDelete(perform: deleteItem)

Pastda esa shu funksiyani yaratamiz:

func deleteItem(indexSet: IndexSet) {
    items.remove(atOffsets: indexSet)
}

Tortib ko'chirish (move)

Xuddi shunday, .onMove qo'shamiz:

.onMove(perform: moveItem)

Va funksiya:

func moveItem(from: IndexSet, to: Int) {
    items.move(fromOffsets: from, toOffset: to)
}

Resume va Play bosib, jonli oldindan ko'rishda tekshiramiz: swipe qilib item o'chirish va edit rejimida tartibini o'zgartirish ishlaydi.


ViewModels papkasi va ListViewModel class

Endi barcha bu mantiqni ViewModelga ko'chiramiz. Sababini tushuntiraylik: ListViewdagi hamma narsa faqat UI uchun bo'lishi kerak — items massivi, deleteItem va moveItem funksiyalari esa UI bilan emas, ma'lumot bilan bog'liq. Shuning uchun bularni ajratib chiqaramiz.

Navigator-da o'ng tugmani bosib, yangi Group yaratamiz va unga ViewModels deb nom beramiz (Models va Views papkalari orasida joylashtiramiz — ViewModel ularni bog'lagani uchun o'rtada turishi mantiqli).

ViewModels papkasida yangi Swift File yaratamiz — ListViewModel deb nomlaymiz.


ListViewModel — asosiy tuzilma

import Foundation

class ListViewModel: ObservableObject {

    @Published var items: [ItemModel] = []

    init() {
        getItems()
    }

    // CRUD funksiyalari

    func getItems() {
        let newItems: [ItemModel] = [
            ItemModel(title: "This is the first title!", isCompleted: false),
            ItemModel(title: "This is the second title!", isCompleted: true),
            ItemModel(title: "This is the third title!", isCompleted: false)
        ]
        items.append(contentsOf: newItems)
    }

    func deleteItem(indexSet: IndexSet) {
        items.remove(atOffsets: indexSet)
    }

    func moveItem(from: IndexSet, to: Int) {
        items.move(fromOffsets: from, toOffset: to)
    }

    func addItem(title: String) {
        let newItem = ItemModel(title: title, isCompleted: false)
        items.append(newItem)
    }

    func updateItem(item: ItemModel) {
        if let index = items.firstIndex(where: { $0.id == item.id }) {
            items[index] = item.updateCompletion()
        }
    }
}

Bir nechta muhim jihatlarni tushuntiraylik:

  • ObservableObject — bu class-ni kuzatib bo'ladigan qiladi, ya'ni undagi o'zgarishlar view-larni yangilaydi.
  • @Publisheditems o'zgarganda, bu o'zgaruvchini kuzatayotgan barcha view-lar qayta chiziladi.
  • @State class-larda ishlatilmaydi, faqat View-larda ishlatiladi. Class-larda buning o'rniga @Published ishlatiladi.
  • init()ListViewModel yaratilgan zahoti getItems() chaqiriladi va boshlang'ich ma'lumotlar yuklanadi.

EnvironmentObject sifatida sozlash

ListViewModelni ilovaning bosh faylida — TodoListApp.swiftda — yaratamiz:

@main
struct TodoListApp: App {
    @StateObject var listViewModel: ListViewModel = ListViewModel()

    var body: some Scene {
        WindowGroup {
            NavigationView {
                ListView()
            }
            .environmentObject(listViewModel)
        }
    }
}

Muhim jihatlar:

  • @StateObject — class-ni kuzatish uchun ishlatiladi (class yaratilganda shu property wrapper tanlanadi).
  • .environmentObject(...) — bu NavigationView ichidagi barcha view-lar shu listViewModelga kirishini ta'minlaydi.
  • ListViewModel ObservableObjectga mos kelishi kerak edi — va biz buni yuqoridagi class tuzilmasida allaqachon qildik.

ListView-ni yangilash

Endi ListView-dan keraksiz narsalarni olib tashlaymiz:

struct ListView: View {
    @EnvironmentObject var listViewModel: ListViewModel

    var body: some View {
        List {
            ForEach(listViewModel.items) { item in
                ListRowView(item: item)
                    .onTapGesture {
                        withAnimation(.linear) {
                            listViewModel.updateItem(item: item)
                        }
                    }
            }
            .onDelete(perform: listViewModel.deleteItem)
            .onMove(perform: listViewModel.moveItem)
        }
        .navigationTitle("Todo List 📝")
        .navigationBarItems(
            leading: EditButton(),
            trailing: NavigationLink("Add", destination: AddView())
        )
    }
}
  • items massivi va deleteItem, moveItem funksiyalari view-dan o'chirildi — ular endi ViewModel-da.
  • @EnvironmentObject orqali listViewModelga kirish imkoniga ega bo'lamiz — uni qo'lda uzatishga hojat yo'q.

Preview-ni tuzatish

Preview-da EnvironmentObject mavjud emas (u faqat App.swift orqali qo'shiladi), shuning uchun preview-ni alohida ta'minlashimiz kerak:

#Preview {
    NavigationView {
        ListView()
    }
    .environmentObject(ListViewModel())
}

AddView-ni yangilash

AddView-da ham listViewModelga kirish kerak bo'ladi:

struct AddView: View {
    @Environment(\.presentationMode) var presentationMode
    @EnvironmentObject var listViewModel: ListViewModel

    @State var textFieldText: String = ""
    @State var alertTitle: String = ""
    @State var showAlert: Bool = false

    var body: some View {
        ScrollView {
            VStack {
                TextField("Type something here...", text: $textFieldText)
                    .padding(.horizontal)
                    .frame(height: 55)
                    .background(Color.gray.brightness(0.3))
                    .cornerRadius(10)

                Button {
                    saveButtonPressed()
                } label: {
                    Text("Save".uppercased())
                        .foregroundColor(.white)
                        .font(.headline)
                        .frame(height: 55)
                        .frame(maxWidth: .infinity)
                        .background(Color.accentColor)
                        .cornerRadius(10)
                }
            }
            .padding(14)
        }
        .navigationTitle("Add an Item 🖊️")
        .alert(isPresented: $showAlert, content: getAlert)
    }

    func saveButtonPressed() {
        if textIsAppropriate() {
            listViewModel.addItem(title: textFieldText)
            presentationMode.wrappedValue.dismiss()
        }
    }

    func textIsAppropriate() -> Bool {
        if textFieldText.count < 3 {
            alertTitle = "Your new todo item must be at least 3 characters long! 😱"
            showAlert.toggle()
            return false
        }
        return true
    }

    func getAlert() -> Alert {
        return Alert(title: Text(alertTitle))
    }
}
  • presentationMode.wrappedValue.dismiss() — item saqlangandan so'ng, oldingi ekranga (ListView-ga) qaytadi.
  • textIsAppropriate() — matn kamida 3 ta belgidan iborat bo'lishligini tekshiradi; aks holda alert ko'rsatiladi.

AddView preview-si

#Preview {
    NavigationView {
        AddView()
    }
    .environmentObject(ListViewModel())
}

ItemModel-ni yangilash — immutable struct va updateCompletion()

Hozirgi holda, updateItem funksiyasida biz:

items[index] = ItemModel(title: item.title, isCompleted: !item.isCompleted)

deb yozsak, yangi ID yaratiladi — bu noto'g'ri, chunki biz aslida xuddi shu itemni yangilayapmiz.

Muammoni hal qilish — idni initializer orqali uzatish

ItemModel-ni quyidagicha yangilaymiz:

struct ItemModel: Identifiable {
    let id: String
    let title: String
    let isCompleted: Bool

    // id optional — berilmasa, avtomatik UUID yaratiladi
    init(id: String = UUID().uuidString, title: String, isCompleted: Bool) {
        self.id = id
        self.title = title
        self.isCompleted = isCompleted
    }

    // Completion holatini teskari qilib, xuddi shu ID bilan yangi model qaytaradi
    func updateCompletion() -> ItemModel {
        return ItemModel(id: id, title: title, isCompleted: !isCompleted)
    }
}

Muhim jihatlar:

  • id: String = UUID().uuidString — yangi item yaratilganda, id berilmasa, avtomatik UUID yaratiladi. Mavjud itemni yangilashda esa xuddi shu id uzatiladi.
  • Immutable struct — barcha o'zgaruvchilar let bilan belgilangan, ya'ni o'zgarmas. Bu yaxshi amaliyot: modelni tasodifiy joydan o'zgartirib yuborish xavfini kamaytiradi. Modelni faqat updateCompletion() funksiyasi orqali yangilash mumkin.
  • updateCompletion() — mavjud id va titleni saqlab, faqat isCompletedni teskari qiladi va yangi ItemModel qaytaradi.

ViewModel-dagi updateItemni tozalash

func updateItem(item: ItemModel) {
    if let index = items.firstIndex(where: { $0.id == item.id }) {
        items[index] = item.updateCompletion()
    }
}

Bu yerda .firstIndex(where:) — massiv ichidagi har bir elementni ko'rib chiqib, ID-si mos keladigan birinchi indexni qaytaradi. Bu index optional (Int?), shuning uchun if let bilan xavfsiz unwrap qilinadi.


CRUD funksiyalari

ViewModel-dagi to'rtta funksiyamiz — ma'lumotlar bilan ishlashning to'rtta asosiy amali deb ataladi:

// CRUD functions
// Create
func addItem(title: String) { ... }

// Read
func getItems() { ... }

// Update
func updateItem(item: ItemModel) { ... }

// Delete
func deleteItem(indexSet: IndexSet) { ... }

CRUD — Create, Read, Update, Delete. Ilovangizda qaysi ma'lumot bilan ishlasangiz (itemlar, foydalanuvchilar, xabarlar va boshqalar), ular uchun doim shu to'rtta turdagi funksiya bo'ladi. Shuning uchun bu to'rttasini yaxshi tushunib olish — har qanday ilova uchun mustahkam zamin yaratadi.


Natija

Endi ilovamiz to'liq ishlaydi:

  • Item-larni swipe qilib o'chirish
  • Edit rejimida tartibini o'zgartirish
  • Yangi item qo'shish (kamida 3 belgi tekshiruvi bilan)
  • Item-ga bosib, tugallangan/tugallanmagan holatini animatsiya bilan almashtirish

Barcha mantiq ListViewModel-da, barcha UI ListView va AddView-da, ma'lumot tuzilmasi esa ItemModel-da — bu MVVM arxitekturasi.

Buy mea coffee