Published on

SwiftUI crypto app — tanga tafsilotlari uchun ma'lumot modeli va xizmat yaratish

Authors

SwiftUI crypto app — tanga tafsilotlari uchun ma'lumot modeli va xizmat yaratish

Oldingi darsda biz DetailView-ga navigatsiya qo'shdik, lekin u faqat tanga nomini ko'rsatmoqda. Bu darsda biz har bir tanga haqida CoinGecko API dan qo'shimcha ma'lumot — tavsif, bloklash vaqti, hashing algoritmi va ko'proq statistika — yuklab olamiz.


1. CoinGecko API — tanga tafsilotlari endpointi

/coins/{id} endpointi har bir tanga uchun ancha batafsil ma'lumot beradi. URL:

https://api.coingecko.com/api/v3/coins/bitcoin?localization=false&tickers=false&market_data=false&community_data=false&developer_data=false&sparkline=false

localization=false — faqat inglizcha ma'lumot, keraksiz barcha ma'lumotlarni ham o'chiramiz.


2. CoinDetailModel yaratish

API javobi katta bo'ladi. Biz faqat kerakli maydonlarni oladi:

import Foundation

struct CoinDetailModel: Codable {
    let id: String?
    let symbol: String?
    let name: String?
    let blockTimeInMinutes: Int?
    let hashingAlgorithm: String?
    let description: Description?
    let links: Links?

    enum CodingKeys: String, CodingKey {
        case id, symbol, name
        case blockTimeInMinutes = "block_time_in_minutes"
        case hashingAlgorithm = "hashing_algorithm"
        case description, links
    }

    struct Description: Codable {
        let en: String?  // Faqat inglizcha tavsif
    }

    struct Links: Codable {
        let homepage: [String]?
        let subredditURL: String?

        enum CodingKeys: String, CodingKey {
            case homepage
            case subredditURL = "subreddit_url"
        }
    }
}

Barcha maydonlar Optional — ba'zi tangalarda ba'zi ma'lumotlar bo'lmasligi mumkin. Bu ilovani xatolikdan himoya qiladi.


3. CoinDetailDataService

services papkasida CoinDetailDataService.swift faylini yaratamiz. Har bir tanga uchun alohida API so'rov yuboriladi:

import Foundation
import Combine

class CoinDetailDataService {

    @Published var coinDetails: CoinDetailModel? = nil

    private let coin: CoinModel
    private var coinDetailSubscription: AnyCancellable?

    init(coin: CoinModel) {
        self.coin = coin
        getCoinDetails()  // Servis yaratilishi bilanoq ma'lumot yuklab olindi
    }

    func getCoinDetails() {
        // URL-da tanganing id-si dinamik qo'yiladi
        guard let url = URL(string:
            "https://api.coingecko.com/api/v3/coins/\(coin.id)?localization=false&tickers=false&market_data=false&community_data=false&developer_data=false&sparkline=false"
        ) else { return }

        coinDetailSubscription = NetworkingManager.download(url: url)
            .decode(type: CoinDetailModel.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: NetworkingManager.handleCompletion,
                receiveValue: { [weak self] returnedCoinDetails in
                    self?.coinDetails = returnedCoinDetails
                    self?.coinDetailSubscription?.cancel()
                }
            )
    }
}

Nima uchun har bir tangasi uchun alohida servis? Asosiy ekranda barcha 250 tanganing tafsilotlari kerak emas. Faqat foydalanuvchi bosgan tanga uchun API so'rov yuborilsa — bu ancha tejamkor.


4. DetailViewModel

core/detail/viewmodels papkasida DetailViewModel.swift:

import Foundation
import Combine

class DetailViewModel: ObservableObject {

    @Published var overviewStatistics: [StatisticModel] = []
    @Published var additionalStatistics: [StatisticModel] = []
    @Published var coinDescription: String? = nil
    @Published var websiteURL: String? = nil
    @Published var redditURL: String? = nil

    private let coin: CoinModel
    private let coinDetailService: CoinDetailDataService
    private var cancellables = Set<AnyCancellable>()

    init(coin: CoinModel) {
        self.coin = coin
        self.coinDetailService = CoinDetailDataService(coin: coin)
        addSubscribers()
    }

    private func addSubscribers() {
        // CoinDetailModel va CoinModel ni birlashtirish -> statistika hosil qilish
        coinDetailService.$coinDetails
            .combineLatest(Just(coin))  // Coin model o'zgarmaydi, Just wrapper kerak
            .map(mapDataToStatistics)
            .sink { [weak self] returnedArrays in
                self?.overviewStatistics = returnedArrays.overview
                self?.additionalStatistics = returnedArrays.additional
            }
            .store(in: &cancellables)

        // Tanga tavsifi va havolalarini ajratish
        coinDetailService.$coinDetails
            .sink { [weak self] returnedCoinDetails in
                self?.coinDescription = returnedCoinDetails?.description?.en?.removingHTMLOccurrences
                self?.websiteURL = returnedCoinDetails?.links?.homepage?.first
                self?.redditURL = returnedCoinDetails?.links?.subredditURL
            }
            .store(in: &cancellables)
    }

    private func mapDataToStatistics(
        coinDetailModel: CoinDetailModel?,
        coinModel: CoinModel
    ) -> (overview: [StatisticModel], additional: [StatisticModel]) {
        let overview = createOverviewArray(coinModel: coinModel)
        let additional = createAdditionalArray(
            coinDetailModel: coinDetailModel,
            coinModel: coinModel
        )
        return (overview, additional)
    }

    // Overview statistikasi: narx, bozor kap, reyting, hajm
    private func createOverviewArray(coinModel: CoinModel) -> [StatisticModel] {
        let price = coinModel.currentPrice.asCurrencyWith6Decimals()
        let priceChange = coinModel.priceChangePercentage24H
        let priceStat = StatisticModel(title: "Joriy narx", value: price, percentageChange: priceChange)

        let marketCap = "$" + (coinModel.marketCap?.formattedWithAbbreviations() ?? "")
        let marketCapChange = coinModel.marketCapChangePercentage24H
        let marketCapStat = StatisticModel(title: "Bozor kapitalizatsiyasi", value: marketCap, percentageChange: marketCapChange)

        let rank = "\(coinModel.rank)"
        let rankStat = StatisticModel(title: "Reyting", value: rank)

        let volume = "$" + (coinModel.totalVolume?.formattedWithAbbreviations() ?? "")
        let volumeStat = StatisticModel(title: "24 soatlik hajm", value: volume)

        return [priceStat, marketCapStat, rankStat, volumeStat]
    }

    // Qo'shimcha statistika: yuqori/past narx, foiz o'zgarishi, bozor kap o'zgarishi, bloklash vaqti
    private func createAdditionalArray(
        coinDetailModel: CoinDetailModel?,
        coinModel: CoinModel
    ) -> [StatisticModel] {
        let high = coinModel.high24H?.asCurrencyWith6Decimals() ?? "n/a"
        let highStat = StatisticModel(title: "24s maksimal narx", value: high)

        let low = coinModel.low24H?.asCurrencyWith6Decimals() ?? "n/a"
        let lowStat = StatisticModel(title: "24s minimal narx", value: low)

        let priceChange = coinModel.priceChange24H?.asCurrencyWith6Decimals() ?? "n/a"
        let pricePercentChange = coinModel.priceChangePercentage24H
        let priceChangeStat = StatisticModel(
            title: "24s narx o'zgarishi",
            value: priceChange,
            percentageChange: pricePercentChange
        )

        let marketCapChange = "$" + (coinModel.marketCapChange24H?.formattedWithAbbreviations() ?? "")
        let marketCapPercentChange = coinModel.marketCapChangePercentage24H
        let marketCapChangeStat = StatisticModel(
            title: "24s bozor kap o'zgarishi",
            value: marketCapChange,
            percentageChange: marketCapPercentChange
        )

        // Bloklash vaqti — faqat CoinDetailModel-dan keladi
        let blockTime = coinDetailModel?.blockTimeInMinutes ?? 0
        let blockTimeString = blockTime == 0 ? "n/a" : "\(blockTime)"
        let blockStat = StatisticModel(title: "Blok vaqti", value: blockTimeString)

        let hashing = coinDetailModel?.hashingAlgorithm ?? "n/a"
        let hashingStat = StatisticModel(title: "Hashing algoritmi", value: hashing)

        return [highStat, lowStat, priceChangeStat, marketCapChangeStat, blockStat, hashingStat]
    }
}

5. DetailView-ga ViewModel ulash

DetailView.swift-ni yangilaymiz:

struct DetailView: View {
    @StateObject private var vm: DetailViewModel

    init(coin: CoinModel) {
        // ViewModel init-da coin kerak, shuning uchun _vm sintaksisi ishlatamiz
        _vm = StateObject(wrappedValue: DetailViewModel(coin: coin))
    }

    var body: some View {
        Text("Yuklanyapti...")  // Keyingi darsda to'ldiriladi
            .navigationTitle(vm.coin.name)
    }
}

6. HTML teglarini olib tashlash

CoinGecko tavsiflarida HTML teglari (<p>, <a>) keladi. String+Extensions.swift-ga:

var removingHTMLOccurrences: String {
    return self.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression)
}

Natija

Simulatorda ilovani ishga tushiring va biror tangaga bosing. Konsolda ko'rasiz:

// Tanga detali yuklanib keldi — ma'lumotlar bor!

Keyingi darsda biz bu ma'lumotlarni LazyVGrid yordamida ekranda chiroyli ko'rsatamiz. 🚀

Buy mea coffee