Published on

SwiftUI crypto app — MVVM va animatsiyalar bilan ro'yxatni saralash

Authors

SwiftUI crypto app — MVVM va animatsiyalar bilan ro'yxatni saralash

Hozircha tangalar bozor kapitalizatsiyasi bo'yicha tartibda keladi. Bu darsda foydalanuvchiga tangalarni reyting, narx va hisoblar bo'yicha o'sish yoki kamayish tartibida saralash imkonini beramiz. Barcha mantiq HomeViewModel-da bo'ladi — view esa faqat bir o'zgaruvchini yangilaydi.


1. SortOption enum

HomeViewModel.swift faylining yuqorisida enum yaratamiz:

enum SortOption {
    case rank           // Reytingdan 1-dan katta (default)
    case rankReversed   // Reytingdan katta-dan kichikka
    case holdings       // Hisoblardan katta-dan kichikka
    case holdingsReversed
    case price          // Narxdan katta-dan kichikka
    case priceReversed
}

Va Published o'zgaruvchi:

@Published var sortOption: SortOption = .holdings  // Ilova boshida — hisoblar bo'yicha

2. filterAndSortCoins funksiyasi

Filtrash bilan saralashni bir funksiyada birlashtirish uchun yangi filterAndSortCoins yaratamiz:

private func filterAndSortCoins(text: String, coins: [CoinModel], sort: SortOption) -> [CoinModel] {
    // Avval filtrlash
    var filteredCoins = filterCoins(text: text, coins: coins)

    // Keyin saralash (in-out yordamida yangi massiv yaratilmaydi)
    sortCoins(sort: sort, coins: &filteredCoins)

    return filteredCoins
}

Subscriber-ni yangilaymiz — $sortOption-ni ham qo'shamiz:

$searchText
    .combineLatest(coinDataService.$allCoins, $sortOption)
    .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
    .map(filterAndSortCoins)
    .sink { [weak self] returnedCoins in
        self?.allCoins = returnedCoins
    }
    .store(in: &cancellables)

3. sortCoins funksiyasi — inout bilan samarali saralash

private func sortCoins(sort: SortOption, coins: inout [CoinModel]) {
    switch sort {
    case .rank, .holdings:
        // Reyting: 1-eng past son — eng avval
        coins.sort { $0.rank < $1.rank }

    case .rankReversed, .holdingsReversed:
        coins.sort { $0.rank > $1.rank }

    case .price:
        // Narx: eng yuqori narx — avval
        coins.sort { $0.currentPrice > $1.currentPrice }

    case .priceReversed:
        coins.sort { $0.currentPrice < $1.currentPrice }
    }
}

inout nima? Oddiy parametr — funksiya ichida nusxa oladi, asl massivni o'zgartirmaydi. inout — asl massivni o'zida saralaydi, yangi massiv yaratmaydi. Bu samaradorlikni oshiradi.

Chaqirish vaqtida & belgisi ishlatiladi: sortCoins(sort: sort, coins: &filteredCoins)


4. Portfolio tangalarini ham saralash

portfolioCoins subscriber qo'shimcha saralashni talab qiladi — faqat holdings holatida:

private func sortPortfolioCoinsIfNeeded(coins: [CoinModel]) -> [CoinModel] {
    // Faqat holdings bo'yicha saralash kerak bo'lganda ishlaydi
    switch sortOption {
    case .holdings:
        return coins.sorted { $0.currentHoldingsValue > $1.currentHoldingsValue }
    case .holdingsReversed:
        return coins.sorted { $0.currentHoldingsValue < $1.currentHoldingsValue }
    default:
        return coins  // Boshqa saralash allCoins-da qilingan
    }
}

Subscriber-da:

$allCoins
    .combineLatest(portfolioDataService.$savedEntities)
    .map(mapAllCoinsToPortfolioCoins)
    .sink { [weak self] returnedCoins in
        guard let self = self else { return }
        self.portfolioCoins = self.sortPortfolioCoinsIfNeeded(coins: returnedCoins)
    }
    .store(in: &cancellables)

Nima uchun bu subscriberda $sortOption-ni qo'shmaymiz? Agar sortOption o'zgarganda bu ham ishlaydigan qilsak, allCoins ham yangilanadi — bu esa portfolio subscriberni ikki marta ishlatadi. Bundan qochish uchun faqat oxirida qo'shimcha saralash qilamiz.


5. Ustun sarlavhalariga animatsiyali o'q qo'shish

HomeView-ning columnTitles ichida har bir ustunni bosiladigan qilamiz:

private var columnTitles: some View {
    HStack {
        // TANGA ustuni
        HStack(spacing: 4) {
            Text("Tanga")
            Image(systemName: "chevron.down")
                // Faqat rank bo'yicha saralanadigan bo'lsa ko'rinadi
                .opacity(vm.sortOption == .rank || vm.sortOption == .rankReversed ? 1.0 : 0.0)
                // Rank bo'lsa — pastga, rankReversed bo'lsa — yuqoriga
                .rotationEffect(Angle(degrees: vm.sortOption == .rank ? 0 : 180))
        }
        .onTapGesture {
            withAnimation(.default) {
                // Ternary operator: agar rank bo'lsa — rankReversed, boshqa holat — rank
                vm.sortOption = vm.sortOption == .rank ? .rankReversed : .rank
            }
        }

        Spacer()

        // HISOBLAR ustuni (faqat portfel tabida)
        if showPortfolio {
            HStack(spacing: 4) {
                Text("Hisoblar")
                Image(systemName: "chevron.down")
                    .opacity(vm.sortOption == .holdings || vm.sortOption == .holdingsReversed ? 1.0 : 0.0)
                    .rotationEffect(Angle(degrees: vm.sortOption == .holdings ? 0 : 180))
            }
            .onTapGesture {
                withAnimation(.default) {
                    vm.sortOption = vm.sortOption == .holdings ? .holdingsReversed : .holdings
                }
            }
        }

        // NARX ustuni
        HStack(spacing: 4) {
            Text("Narx")
            Image(systemName: "chevron.down")
                .opacity(vm.sortOption == .price || vm.sortOption == .priceReversed ? 1.0 : 0.0)
                .rotationEffect(Angle(degrees: vm.sortOption == .price ? 0 : 180))
        }
        .frame(width: UIScreen.main.bounds.width / 3.5, alignment: .trailing)
        .onTapGesture {
            withAnimation(.default) {
                vm.sortOption = vm.sortOption == .price ? .priceReversed : .price
            }
        }

        // Yangilash tugmasi
        Button {
            withAnimation(.linear(duration: 2.0)) {
                vm.reloadData()
            }
        } label: {
            Image(systemName: "goforward")
        }
        .rotationEffect(Angle(degrees: vm.isLoading ? 360 : 0), anchor: .center)
    }
    .font(.caption)
    .foregroundColor(Color.theme.secondaryText)
    .padding(.horizontal)
}

6. Simulatorda sinash

Ilovani ishga tushiring:

  • Portfel tabida: "Hisoblar" ni bossangiz — eng katta hisoblar yuqorida ko'rinadi
  • O'q animatsiyasi: o'q qarama-qarshi tomonga buriladi
  • Narx bo'yicha: asosiy tabda "Narx" ni bossangiz — tartib o'zgaradi
  • Tanga: "Tanga" ni bossangiz — CoinGecko reytingi bo'yicha

"Faqat bitta sortOption o'zgaruvchisini o'zgartiramiz — Combine barcha subscriberlarni avtomatik ishga tushiradi va hamma narsa yangilanadi. Bu SwiftUI + Combine kombinatsiyasining sehrli tomonlari!"

Keyingi darsda biz batafsil ekran (DetailView) ga navigatsiyani qo'shamiz. 🚀

Buy mea coffee