Display downloaded data in grid format with reusable components | SwiftUI Crypto App #20
private func addSubscriber() {
coinDetailDataService
.$coinDetails
.combineLatest($coin)
.map(mapDataToStatistics)
.sink { [weak self] returnedArray in
self?.overviewStatistics = returnedArray.overview
self?.additionalStatistics = returnedArray.additional
}
.store(in: &cancellables)
}
private var overviewGrid: some View {
LazyVGrid(columns: columns, alignment: .leading, spacing: spacing, pinnedViews: []) {
ForEach(viewModel.overviewStatistics) { stat in
StatisticView(stat: stat)
}
}
}
import Foundation
import Combine
class DetailViewModel: ObservableObject {
@Published var overviewStatistics: [StatisticModel] = []
@Published var additionalStatistics: [StatisticModel] = []
@Published var coinDetails: CoinDetailModel?
@Published var coin: CoinModel
private let coinDetailDataService: CoinDetailDataService
private var cancellables = Set<AnyCancellable>()
init(coin: CoinModel) {
self.coin = coin
self.coinDetailDataService = CoinDetailDataService(coin: coin)
addSubscriber()
}
private func mapDataToStatistics(coinDetailModel: CoinDetailModel?, coinModel: CoinModel) -> (overview: [StatisticModel], additional: [StatisticModel]) {
// overview
let overviewArray = createOverviewArray(coinModel: coinModel)
// additional
let additionalArray = createAdditionalArray(coinDetailModel: coinDetailModel, coinModel: coinModel)
return (overviewArray, additionalArray)
}
private func createAdditionalArray(coinDetailModel: CoinDetailModel?, coinModel: CoinModel) -> [StatisticModel] {
let high = coinModel.high24H?.asCurrencyWith6Decimals() ?? "n/a"
let highStat = StatisticModel(title: "24h High", value: high)
let low = coinModel.low24H?.asCurrencyWith6Decimals() ?? "n/a"
let lowStat = StatisticModel(title: "24h Low", value: low)
let priceChange = coinModel.priceChange24H?.asCurrencyWith6Decimals() ?? "n/a"
let pricePercentChange2 = coinModel.priceChangePercentage24H
let priceChangeStat = StatisticModel(title: "24h Pirce Change", value: priceChange, percentageChange: pricePercentChange2)
let marketCapChange = "$" + (coinModel.marketCapChange24H?.formattedWithAbbreviations() ?? "")
let marketCapPercentChange2 = coinModel.marketCapChangePercentage24H
let marketCapChangeStat = StatisticModel(title: "Market Cap Change", value: marketCapChange, percentageChange: marketCapPercentChange2)
let blockTime = coinDetailModel?.blockTimeInMinutes ?? 0
let blockTimeString = blockTime == 0 ? "n/a" : "\(blockTime)"
let blockStat = StatisticModel(title: "Block Time", value: blockTimeString)
let hashing = coinDetailModel?.hashingAlgorithm ?? "n/a"
let hashingStat = StatisticModel(title: "Hashing Algorithm", value: hashing)
let additionalArray = [highStat, lowStat, priceChangeStat, marketCapChangeStat, blockStat, hashingStat]
return additionalArray
}
private func createOverviewArray(coinModel: CoinModel) -> [StatisticModel] {
let price = coinModel.currentPrice.asCurrencyWith6Decimals()
let pricePercentChange = coinModel.priceChangePercentage24H
let priceStat = StatisticModel(title: "Price Change", value: price, percentageChange: pricePercentChange)
let marketCap = "$" + (coinModel.marketCap?.formattedWithAbbreviations() ?? "")
let marketCapPercentChange = coinModel.marketCapChangePercentage24H
let marketCapStat = StatisticModel(title: "Market Capitalization", value: marketCap, percentageChange: marketCapPercentChange)
let rank = "\(coinModel.rank)"
let rankStat = StatisticModel(title: "Rank", value: rank)
let volume = "$" + (coinModel.totalVolume?.formattedWithAbbreviations() ?? "")
let volumeStat = StatisticModel(title: "Volume", value: volume)
let overviewArray = [priceStat, marketCapStat, rankStat, volumeStat]
return overviewArray
}
private func addSubscriber() {
coinDetailDataService
.$coinDetails
.combineLatest($coin)
.map(mapDataToStatistics)
.sink { [weak self] returnedArray in
self?.overviewStatistics = returnedArray.overview
self?.additionalStatistics = returnedArray.additional
}
.store(in: &cancellables)
}
}
@Published
로 전달import SwiftUI
struct DetailView: View {
@StateObject private var viewModel: DetailViewModel
let coin: CoinModel
private let columns: [GridItem] = [
GridItem(.flexible()),
GridItem(.flexible()),
]
private let spacing: CGFloat = 30
init(coin: CoinModel) {
self.coin = coin
self._viewModel = StateObject(wrappedValue: .init(coin: coin))
}
var body: some View {
ScrollView {
VStack(spacing: 20) {
Text("")
.frame(height: 150)
overviewTitle
Divider()
overviewGrid
additionalTitle
Divider()
additionalGrid
}
.padding()
}
.navigationTitle("\(viewModel.coin.name)")
}
}
extension DetailView {
private var overviewTitle: some View {
Text("Overview")
.font(.title)
.bold()
.foregroundColor(Color.theme.accent)
.frame(maxWidth: .infinity, alignment: .leading)
}
private var overviewGrid: some View {
LazyVGrid(columns: columns, alignment: .leading, spacing: spacing, pinnedViews: []) {
ForEach(viewModel.overviewStatistics) { stat in
StatisticView(stat: stat)
}
}
}
private var additionalTitle: some View {
Text("Additional Detail")
.font(.title)
.bold()
.foregroundColor(Color.theme.accent)
.frame(maxWidth: .infinity, alignment: .leading)
}
private var additionalGrid: some View {
LazyVGrid(columns: columns, alignment: .leading, spacing: spacing, pinnedViews: []) {
ForEach(viewModel.additionalStatistics) { stat in
StatisticView(stat: stat)
}
}
}
}
viewModel.overviewStatistics
또는 viewModel.additionalStatistics
가 @Published
프로토콜을 따르는 데이터 퍼블리셔이기 때문에 해당 변수 값이 변경될 때마다 뷰가 새롭게 리렌더링LazyGrid
형태로 수직 형태로 칼럼 2개 고정