Published on

SwiftUI crypto app — yakuniy optimizatsiyalar

Authors

SwiftUI crypto app — yakuniy optimizatsiyalar

Ilova deyarli tayyor. Bu darsda kichik lekin muhim optimizatsiyalar qilamiz:

  • iPad-da to'g'ri ko'rinishi
  • Network so'rovlarini background threadda decode qilish
  • retry() bilan muvaffaqiyatsiz so'rovlarni qayta urinish
  • Bo'sh portfel uchun foydalanuvchiga xabar

1. iPad qo'llab-quvvatlash

iPad simulatorida ilovani ishga tushiring. Muammo: NavigationView iPad-da ikkita panelda ko'rinadi (split view) — chap tomonida asosiy ekran, o'ng tomonida detail ekran. Bu iphone-dan farqli.

SwiftUICryptoApp.swift-da bir qator qo'shish yetarli:

NavigationView {
    HomeView()
}
.navigationViewStyle(StackNavigationViewStyle())
// ↑ Bu iPad-ni ham iPhone kabi "stack" navigatsiyasiga majbur qiladi

Endi iPad ham iPhone kabi ishlaydi. LazyVGrid va GeometryReader avtomatik o'lchamga moslashadi — qo'shimcha hech narsa kerak emas.


2. NetworkingManager optimizatsiyasi

Joriy holat:

// NetworkingManager.swift
static func download(url: URL) -> AnyPublisher<Data, Error> {
    return URLSession.shared.dataTaskPublisher(for: url)
        .subscribe(on: DispatchQueue.global(qos: .default))  // ← keraksiz!
        .tryMap({ try handleURLResponse(output: $0, url: url) })
        .receive(on: DispatchQueue.main)  // ← bu ham to'g'ri joyda emas
        .eraseToAnyPublisher()
}

Muammo: URLSession.dataTaskPublisher allaqachon background threadda ishlaydi. subscribe(on:) kerak emas. Va receive(on: DispatchQueue.main) decode-dan oldin kelsa — decode main threadda bo'ladi.

Optimizatsiyalangan versiya:

static func download(url: URL) -> AnyPublisher<Data, Error> {
    return URLSession.shared.dataTaskPublisher(for: url)
        // subscribe(on:) olib tashlandi — allaqachon background-da
        .tryMap({ try handleURLResponse(output: $0, url: url) })
        .retry(3)  // Muvaffaqiyatsiz bo'lsa — 3 marta qayta urinish
        .eraseToAnyPublisher()
    // receive(on: main) service fayllariga ko'chirildi
}

3. Har bir service faylida receive(on:) qo'shish

Decode background-da bo'lgach, UI-ni yangilashdan oldin main-ga qaytish kerak.

CoinDataService.swift:

coinSubscription = NetworkingManager.download(url: url)
    .decode(type: [CoinModel].self, decoder: JSONDecoder())
    .receive(on: DispatchQueue.main)  // ← decode KEYIN main-ga o'tadi
    .sink(receiveCompletion: NetworkingManager.handleCompletion) { [weak self] returnedCoins in
        self?.allCoins = returnedCoins
    }

Xuddi shunday CoinDetailDataService, MarketDataService, CoinImageService fayllarida ham decode-dan keyin .receive(on: DispatchQueue.main) qo'shiladi.

Nima uchun muhim? Agar background threaddan @Published o'zgaruvchini yangilasak — "Publishing changes from background threads is not allowed" xatoligi chiqadi. Combine bu qoidani qat'iy qo'llaydi.


4. retry(3) — 3 marta qayta urinish

URLSession.shared.dataTaskPublisher(for: url)
    .tryMap({ try handleURLResponse(output: $0, url: url) })
    .retry(3)  // Agar server xatolik bersa — 3 marta qayta so'rov yuboradi
    .eraseToAnyPublisher()

Qanday ishlaydi? Agar tryMap xatolik qaytarsa — retry(3) avtomatik dataTaskPublisher-ni qaytadan ishga tushiradi. 3 urinishdan keyin ham muvaffaqiyatsiz bo'lsa — handleCompletion-ga xatolik keladi.

Hozirgi ilovamizda xatolik yo'q, lekin serverda muammo bo'lganda bu qator foydalanuvchiga uzluksiz tajriba beradi.


5. Bo'sh portfel xabari

Portfelda tangalar bo'lmasa — ekran bo'sh va foydalanuvchi nima qilish kerakligini bilmaydi. HomeView-da:

private var portfolioCoinsList: some View {
    ZStack(alignment: .top) {
        if vm.portfolioCoins.isEmpty && vm.searchText.isEmpty {
            // Bo'sh portfel xabari
            portfolioEmptyText
        } else {
            // Tangalar ro'yxati
            List {
                ForEach(vm.portfolioCoins) { coin in
                    CoinRowView(coin: coin, showHoldingsColumn: true)
                        .onTapGesture { segue(coin: coin) }
                }
            }
            .listStyle(PlainListStyle())
        }
    }
    .transition(.opacity.animation(.easeIn))
}

private var portfolioEmptyText: some View {
    Text("Hali portfelingizga tanga qo'shmadingiz! Boshlash uchun + tugmasini bosing 🧐!")
        .font(.callout)
        .fontWeight(.medium)
        .foregroundColor(Color.theme.accent)
        .multilineTextAlignment(.center)
        .padding(50)
}

6. ContentView-ni o'chirish

Kurs boshida ranglarni sinash uchun yaratilgan ContentView.swift endi kerak emas. O'chirib tashlang — ilova yaxshi ishlashda davom etadi.


7. Portfel foiz hisoblashidagi xatolik tuzatish

HomeViewModel-dagi mapGlobalMarketData funksiyasida percentageChange hisoblashda qo'shimcha × 100 kerak emas:

// Noto'g'ri:
let percentageChange = ((portfolioValue - previousValue) / previousValue) * 100

// To'g'ri — priceChangePercentage24H allaqachon foiz (masalan 13.5):
let percentageChange = (portfolioValue - previousValue) / previousValue

Chunki StatisticModel foizni formatlab ko'rsatadi (13.513.50%). × 100 qo'shsak, 1350% ko'rinadi — bu noto'g'ri.


Yakuniy holat

Ilovangizni yana bir marta ishga tushiring. Keyin quyidagilarni tekshiring:

XususiyatHolat
App ikonkasi✅ Ko'rinadi
Launch animatsiyasi✅ To'lqinlanuvchi harflar
Saralash✅ Reyting, narx, hisoblar
DetailView✅ Grafik + statistika + tavsif
Core Data✅ Portfel saqlanadi
iPad✅ Stack navigation
Background threading✅ Decode background-da
Network retry✅ 3 marta urinish
Bo'sh portfel xabari✅ Foydalanuvchiga yo'l-yo'riq

"Butun ilova 27 dars davomida bir nol dan qurildi. MVVM arxitekturasi, Combine, Core Data, custom animations — hammasi SwiftUI-ning kuchi bilan. Bu sizning portfolio uchun juda kuchli loyiha!"

Keyingi darsda rang sxemasini yangilaymiz. 🚀

Buy mea coffee