데이터 다운까지는 완료했죠
이제 update해봅시다
DetailViewModel에 coin을 받는 상수 하나 선언하고
이 coin의 이름을 navigationTitle로 쓸거!
LazyVGrid추가!
DetailViewModel로 와서
overviewStatistics랑 additional 모델 어레이 추가해줌
import Foundation
import Combine
class DetailViewModel: ObservableObject {
@Published var overviewStatistics: [StatisticModel] = []
@Published var additionalStatistics: [StatisticModel] = []
@Published var coin: CoinModel
private let coinDetailService: CoinDetailDataService
private var cancellables = Set<AnyCancellable>()
init(coin: CoinModel) {
self.coin = coin
self.coinDetailService = CoinDetailDataService(coin: coin)
self.addSubscribers()
}
private func addSubscribers() {
coinDetailService.$coinDetails
.combineLatest($coin)
.map({ (coinDetailModel, coinModel) -> (overview: [StatisticModel], additional: [StatisticModel]) in
// overview
let price = coinModel.currentPrice.asCurrencyWith6Decimals()
let pricePercentChange = coinModel.priceChangePercentage24H
let priceStat = StatisticModel(title: "Current Price", 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: [StatisticModel] = [
priceStat, marketCapStat, rankStat, volumeStat
]
// additional
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?.asCurrencyWith2Decimals() ?? "n/a"
let pricePercentChange2 = coinModel.priceChangePercentage24H
let priceChangeStat = StatisticModel(title: "24h Price Change", value: priceChange, percentageChange: pricePercentChange2)
let marketCapChange = "$" + (coinModel.marketCapChange24H?.formattedWithAbbreviations() ?? "")
let marketCapPercentChange2 = coinModel.marketCapChangePercentage24H
let marketCapChangeStat = StatisticModel(title: "24h 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: [StatisticModel] = [
highStat, lowStat, priceChangeStat, marketCapChangeStat, blockStat, hashingStat
]
return (overviewArray, additionalArray)
})
.sink { [weak self] returnedArrays in
self?.overviewStatistics = returnedArrays.overview
self?.additionalStatistics = returnedArrays.additional
}
.store(in: &cancellables)
}
}
자료들 넘겨주고
아까 ForEach로 그리던 뷰에 넘겨주자!
실행하면 잘 나오는 걸 볼 수 있음!
이제 차트를 그려봅시다
이제 좀 수학적인 계산을 해야함 ㅋㅋ
import SwiftUI
struct ChartView: View {
let data: [Double]
let maxY: Double
let minY: Double
let lineColor: Color
init(coin: CoinModel) {
data = coin.sparklineIn7D?.price ?? []
maxY = data.max() ?? 0
minY = data.min() ?? 0
let priceChange = (data.last ?? 0) - (data.first ?? 0)
lineColor = priceChange > 0 ? Color.theme.green : Color.theme.red
}
var body: some View {
VStack {
chartView
.frame(height: 200)
.background(chartBackground)
.overlay(alignment: .leading) { chartYAxis }
}
}
}
extension ChartView {
private var chartView: some View {
GeometryReader { geometry in
Path { path in
for index in data.indices {
let xPosition = geometry.size.width / CGFloat(data.count) * CGFloat(index + 1)
let yAxis = maxY - minY
let yPosition = (1 - CGFloat((data[index] - minY) / yAxis)) * geometry.size.height
if index == 0 {
path.move(to: CGPoint(x: xPosition, y: yPosition))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
}
.stroke(lineColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
}
}
private var chartBackground: some View {
VStack {
Divider()
Spacer()
Divider()
Spacer()
Divider()
}
}
private var chartYAxis: some View {
VStack {
Text(maxY.formattedWithAbbreviations())
Spacer()
Text(((maxY + minY) / 2).formattedWithAbbreviations())
Spacer()
Text(minY.formattedWithAbbreviations())
}
}
}
struct ChartView_Previews: PreviewProvider {
static var previews: some View {
ChartView(coin: dev.coin)
}
}
Path를 이용해서 받은 데이터들을 좌표로 표시해줬다!
이제 coinModel에 있는 데이터 중 date값을 받아서 표시도 해봅시다
extension으로 만들면 되겠죠
Date를 string으로 바꿔주는 것도 필요함
조금 이쁘게 만들어봅시다
그리고 차트를 애니메이션 시켜볼거
.trim을 이용해서!
그리고 바디에서 .onAppear될 때 trim 값이 조절되게!
shadow 추가해서 살짝 빛나는 효과도 만들어줌!
디테일뷰로 돌아와서 ChartView를 넣어주고!
실행하면
캬~!
CoinDetailModel에서 아직 사용하지 않은 descriptiond이랑 links를 사용해봅시다
새로운 subscribers를 만들어줌
그리고 DetailView에서 description을 가지는 텍스트를 작성하자
html코드도 같이 딸려오고 있음 이거 빼주죠
import Foundation
extension String {
var removingHTMLOccurances: String {
return self.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil)
}
}
CoinDetailModel에서 새로운 변수 하나 만들어주고
DetailViewModel에서 sink할 때 받아주게끔 만들어줬다
글구 animate값에 따라 여러 줄 나오고 줄일 수 있게 해줌
websiteSection도 만들어서 Link를 클릭하면 사파리 켜지게 해주고!
SettingsView를 만들어줄건데 info 버튼 누르면 팝업될거!
그래서 진입점이 달라지기 때문에 NavigationView 넣어줘야한다
struct SettingsView: View {
@Environment(\.dismiss) var dismiss
let defaultURL = URL(string: "https://www.google.com")!
let youtubeURL = URL(string: "https://www.youtube.com/c/swiftfulthinking")!
let coffeeURL = URL(string: "https://www.buymeacoffee.com/nicksarno")!
let coingeckoURL = URL(string: "https://www.coingecko.com")!
let personalURL = URL(string: "https://www.nicksarno.com")!
var body: some View {
NavigationView {
List {
swiftfulthinkingSection
coinGeckoSection
developerSection
applicationSection
}
.font(.headline)
.tint(.blue)
.listStyle(.grouped)
.navigationTitle("Settings")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
XmarkButton(dismiss: _dismiss)
}
}
}
}
}
extension SettingsView {
private var swiftfulthinkingSection: some View {
Section {
VStack(alignment: .leading) {
Image("logo")
.resizable()
.frame(width: 100,height: 100)
.clipShape(RoundedRectangle(cornerRadius: 20))
Text("This app was made by following a @SwiftfulThinking course on Youtube. It uses MVVM Architecture, Combine, and CoreData!")
.font(.callout)
.fontWeight(.medium)
.foregroundColor(Color.theme.accent)
}
.padding(.vertical)
Link("Subscribe on Youtube 🥳", destination: youtubeURL)
Link("Support his coffee addiction ☕️", destination: coffeeURL)
} header: {
Text("Swiftful Thinking")
}
}
private var coinGeckoSection: some View {
Section {
VStack(alignment: .leading) {
Image("coingecko")
.resizable()
.scaledToFit()
.frame(height: 100)
.clipShape(RoundedRectangle(cornerRadius: 20))
Text("The cryptocurrency data that is used in this app comes from a free API from CoinGecko! Prices may be slightly delayed.")
.font(.callout)
.fontWeight(.medium)
.foregroundColor(Color.theme.accent)
}
.padding(.vertical)
Link("Visit CoinGecko 🥳", destination: coingeckoURL)
} header: {
Text("CoinGecko")
}
}
private var developerSection: some View {
Section {
VStack(alignment: .leading) {
Image("logo")
.resizable()
.frame(width: 100,height: 100)
.clipShape(RoundedRectangle(cornerRadius: 20))
Text("This app was developed by woozoobro. It uses SwiftUI and is written 100% in Swift. The project benefits from multi-threading, publishers/subscribers, and data persistance.")
.font(.callout)
.fontWeight(.medium)
.foregroundColor(Color.theme.accent)
}
.padding(.vertical)
Link("Visit Website 🥳", destination: personalURL)
} header: {
Text("Developer")
}
}
private var applicationSection: some View {
Section {
Link("Terms of Service", destination: defaultURL)
Link("Privacy Policy", destination: defaultURL)
Link("Company Website", destination: defaultURL)
Link("Learn More", destination: defaultURL)
} header: {
Text("Application")
}
}
}
그리고 홈뷰로 돌아와서 .sheet으로 방금 만든 뷰를 띄워줄건데
이미 Color.theme.background에 .sheet이 달려 있어서 여기말고 VStack에 붙여줄거