1. 创建货币详情数据模型类 CoinDetailModel.swift
import Foundation
// JSON Data
/*
URL:
https://api.coingecko.com/api/v3/coins/bitcoin?localization=false&tickers=false&market_data=false&community_data=false&developer_data=false&sparkline=false
Response:
{
"id": "bitcoin",
"symbol": "btc",
"name": "Bitcoin",
"asset_platform_id": null,
"platforms": {
"": ""
},
"detail_platforms": {
"": {
"decimal_place": null,
"contract_address": ""
}
},
"block_time_in_minutes": 10,
"hashing_algorithm": "SHA-256",
"categories": [
"Cryptocurrency",
"Layer 1 (L1)"
],
"public_notice": null,
"additional_notices": [],
"description": {
"en": "Bitcoin is the first successful internet money based on peer-to-peer technology; whereby no central bank or authority is involved in the transaction and production of the Bitcoin currency. It was created by an anonymous individual/group under the name, Satoshi Nakamoto. The source code is available publicly as an open source project, anybody can look at it and be part of the developmental process.\r\n\r\nBitcoin is changing the way we see money as we speak. The idea was to produce a means of exchange, independent of any central authority, that could be transferred electronically in a secure, verifiable and immutable way. It is a decentralized peer-to-peer internet currency making mobile payment easy, very low transaction fees, protects your identity, and it works anywhere all the time with no central authority and banks.\r\n\r\nBitcoin is designed to have only 21 million BTC ever created, thus making it a deflationary currency. Bitcoin uses the <a href=\"https://www.coingecko.com/en?hashing_algorithm=SHA-256\">SHA-256</a> hashing algorithm with an average transaction confirmation time of 10 minutes. Miners today are mining Bitcoin using ASIC chip dedicated to only mining Bitcoin, and the hash rate has shot up to peta hashes.\r\n\r\nBeing the first successful online cryptography currency, Bitcoin has inspired other alternative currencies such as <a href=\"https://www.coingecko.com/en/coins/litecoin\">Litecoin</a>, <a href=\"https://www.coingecko.com/en/coins/peercoin\">Peercoin</a>, <a href=\"https://www.coingecko.com/en/coins/primecoin\">Primecoin</a>, and so on.\r\n\r\nThe cryptocurrency then took off with the innovation of the turing-complete smart contract by <a href=\"https://www.coingecko.com/en/coins/ethereum\">Ethereum</a> which led to the development of other amazing projects such as <a href=\"https://www.coingecko.com/en/coins/eos\">EOS</a>, <a href=\"https://www.coingecko.com/en/coins/tron\">Tron</a>, and even crypto-collectibles such as <a href=\"https://www.coingecko.com/buzz/ethereum-still-king-dapps-cryptokitties-need-1-billion-on-eos\">CryptoKitties</a>."
},
"links": {
"homepage": [
"http://www.bitcoin.org",
"",
""
],
"blockchain_site": [
"https://blockchair.com/bitcoin/",
"https://btc.com/",
"https://btc.tokenview.io/",
"https://www.oklink.com/btc",
"https://3xpl.com/bitcoin",
"",
"",
"",
"",
""
],
"official_forum_url": [
"https://bitcointalk.org/",
"",
""
],
"chat_url": [
"",
"",
""
],
"announcement_url": [
"",
""
],
"twitter_screen_name": "bitcoin",
"facebook_username": "bitcoins",
"bitcointalk_thread_identifier": null,
"telegram_channel_identifier": "",
"subreddit_url": "https://www.reddit.com/r/Bitcoin/",
"repos_url": {
"github": [
"https://github.com/bitcoin/bitcoin",
"https://github.com/bitcoin/bips"
],
"bitbucket": []
}
},
"image": {
"thumb": "https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579",
"small": "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579",
"large": "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579"
},
"country_origin": "",
"genesis_date": "2009-01-03",
"sentiment_votes_up_percentage": 73.21,
"sentiment_votes_down_percentage": 26.79,
"watchlist_portfolio_users": 1326950,
"market_cap_rank": 1,
"coingecko_rank": 1,
"coingecko_score": 83.151,
"developer_score": 99.241,
"community_score": 83.341,
"liquidity_score": 100.011,
"public_interest_score": 0.073,
"public_interest_stats": {
"alexa_rank": 9440,
"bing_matches": null
},
"status_updates": [],
"last_updated": "2023-08-11T08:43:13.856Z"
}
*/
/// 交易货币详情模型
struct CoinDetailModel: Codable {
let id, symbol, 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
}
/// 去掉 HTML 链接的描述
var readableDescription: String? {
return description?.en?.removingHTMLOccurances
}
}
struct Description: Codable {
let en: String?
}
struct Links: Codable {
let homepage: [String]?
let subredditURL: String?
enum CodingKeys: String, CodingKey {
case homepage
case subredditURL = "subreddit_url"
}
}
2. 创建货币详情数据服务类 CoinDetailDataService.swift
import Foundation
import Combine
class CoinDetailDataService{
@Published var coinDetails: CoinDetailModel? = nil
var coinDetailSubscription: AnyCancellable?
let coin: CoinModel
init(coin: CoinModel) {
self.coin = coin
getCoinDetails()
}
func getCoinDetails(){
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] returnCoinDetails in
self?.coinDetails = returnCoinDetails
self?.coinDetailSubscription?.cancel()
})
}
}
3. 创建货币详情 ViewModel 类,DetailViewModel.swift
import Foundation
import Combine
class DetailViewModel: ObservableObject {
@Published var overviewStatistics: [StatisticModel] = []
@Published var additionalStatistics: [StatisticModel] = []
@Published var description: String? = nil
@Published var websiteURL: String? = nil
@Published var redditURL: String? = nil
@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(mapDataToStatistics)
.sink {[weak self] returnedArrays in
self?.overviewStatistics = returnedArrays.overview
self?.additionalStatistics = returnedArrays.additional
}
.store(in: &cancellables)
coinDetailService.$coinDetails
.sink {[weak self] returnedCoinDetails in
self?.description = returnedCoinDetails?.readableDescription
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 overviewArray = createOvervierArray(coinModel: coinModel)
let additionalArray = createAdditionalArray(coinModel: coinModel, coinDetailModel: coinDetailModel)
return (overviewArray, additionalArray)
}
private func createOvervierArray(coinModel: CoinModel) -> [StatisticModel]{
let price = coinModel.currentPrice.asCurrencyWith6Decimals()
let pricePercentChange = coinModel.priceChangePercentage24H
let priceStat = StatisticModel(title: "Current Price", value: price, percentageChange: pricePercentChange)
let marketCap = "import Foundation
import Combine
/// 交易货币详情 ViewModel
class DetailViewModel: ObservableObject {
/// 概述统计模型数组
@Published var overviewStatistics: [StatisticModel] = []
/// 附加统计数据数组
@Published var additionalStatistics: [StatisticModel] = []
/// 货币描述
@Published var description: String? = nil
/// 货币官网网站
@Published var websiteURL: String? = nil
/// 货币社区网站
@Published var redditURL: String? = nil
/// 交易货币模型
@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(mapDataToStatistics)
.sink {[weak self] returnedArrays in
self?.overviewStatistics = returnedArrays.overview
self?.additionalStatistics = returnedArrays.additional
}
.store(in: &cancellables)
// 订阅货币详情数据
coinDetailService.$coinDetails
.sink {[weak self] returnedCoinDetails in
self?.description = returnedCoinDetails?.readableDescription
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 overviewArray = createOvervierArray(coinModel: coinModel)
// 附加信息
// 当前货币附加信息
let additionalArray = createAdditionalArray(coinModel: coinModel, coinDetailModel: coinDetailModel)
// 返回数组
return (overviewArray, additionalArray)
}
/// 创建概述信息数组
private func createOvervierArray(coinModel: CoinModel) -> [StatisticModel]{
// 当前交易货币价格
let price = coinModel.currentPrice.asCurrencyWith6Decimals()
// 当前交易货币价格 24 小时的变化百分比
let pricePercentChange = coinModel.priceChangePercentage24H
// 当前交易货币价格 统计信息
let priceStat = StatisticModel(title: "Current Price", value: price, percentageChange: pricePercentChange)
// 市值 价格
let marketCap = "$" + (coinModel.marketCap?.formattedWithAbbreviations() ?? "")
// 市值 24 小时变化百分比
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)
// 当前货币概述信息
return [priceStat, marketCapStat, rankStat, volumeStat]
}
/// 创建附加信息数组
private func createAdditionalArray(coinModel: CoinModel, coinDetailModel: CoinDetailModel?) -> [StatisticModel]{
// 24 小时内最高点
let high = coinModel.high24H?.asCurrencyWith6Decimals() ?? "n/a"
// 最高点 统计信息
let highStat = StatisticModel(title: "24h High", value: high)
// 24 小时内最低点
let low = coinModel.low24H?.asCurrencyWith6Decimals() ?? "n/a"
// 最低点 统计信息
let lowStat = StatisticModel(title: "24h Low", value: low)
// 24 小时内价格变化
let priceChange = coinModel.priceChange24H?.asCurrencyWith6Decimals() ?? "n/a"
// 当前交易货币 24 小时的价格变化百分比
let pricePercentChange2 = coinModel.priceChangePercentage24H
// 24 小时内价格变化 统计信息
let priceChangeStat = StatisticModel(title: "24h Price Change", value: priceChange, percentageChange: pricePercentChange2)
// 24 小时内市值变化值
let marketCapChange = "$" + (coinModel.marketCapChange24H?.formattedWithAbbreviations() ?? "")
// 市值 24 小时变化百分比
let marketCapPercentChange2 = coinModel.marketCapChangePercentage24H
// 24 小时内市值变换 统计信息
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 blockTimeStat = StatisticModel(title: "Block Time", value: blockTimeString)
// 哈希/散列 算法
let hashing = coinDetailModel?.hashingAlgorithm ?? "n/a"
let hashingStat = StatisticModel(title: "Hashing Algorithm", value: hashing)
// 当前货币附加信息
return [highStat, lowStat, priceChangeStat, marketCapChangeStat, blockTimeStat, hashingStat]
}
}
quot; + (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)
return [priceStat, marketCapStat, rankStat, volumeStat]
}
private func createAdditionalArray(coinModel: CoinModel, coinDetailModel: CoinDetailModel?) -> [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 Price Change", value: priceChange, percentageChange: pricePercentChange2)
let marketCapChange = "import Foundation
import Combine
/// 交易货币详情 ViewModel
class DetailViewModel: ObservableObject {
/// 概述统计模型数组
@Published var overviewStatistics: [StatisticModel] = []
/// 附加统计数据数组
@Published var additionalStatistics: [StatisticModel] = []
/// 货币描述
@Published var description: String? = nil
/// 货币官网网站
@Published var websiteURL: String? = nil
/// 货币社区网站
@Published var redditURL: String? = nil
/// 交易货币模型
@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(mapDataToStatistics)
.sink {[weak self] returnedArrays in
self?.overviewStatistics = returnedArrays.overview
self?.additionalStatistics = returnedArrays.additional
}
.store(in: &cancellables)
// 订阅货币详情数据
coinDetailService.$coinDetails
.sink {[weak self] returnedCoinDetails in
self?.description = returnedCoinDetails?.readableDescription
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 overviewArray = createOvervierArray(coinModel: coinModel)
// 附加信息
// 当前货币附加信息
let additionalArray = createAdditionalArray(coinModel: coinModel, coinDetailModel: coinDetailModel)
// 返回数组
return (overviewArray, additionalArray)
}
/// 创建概述信息数组
private func createOvervierArray(coinModel: CoinModel) -> [StatisticModel]{
// 当前交易货币价格
let price = coinModel.currentPrice.asCurrencyWith6Decimals()
// 当前交易货币价格 24 小时的变化百分比
let pricePercentChange = coinModel.priceChangePercentage24H
// 当前交易货币价格 统计信息
let priceStat = StatisticModel(title: "Current Price", value: price, percentageChange: pricePercentChange)
// 市值 价格
let marketCap = "$" + (coinModel.marketCap?.formattedWithAbbreviations() ?? "")
// 市值 24 小时变化百分比
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)
// 当前货币概述信息
return [priceStat, marketCapStat, rankStat, volumeStat]
}
/// 创建附加信息数组
private func createAdditionalArray(coinModel: CoinModel, coinDetailModel: CoinDetailModel?) -> [StatisticModel]{
// 24 小时内最高点
let high = coinModel.high24H?.asCurrencyWith6Decimals() ?? "n/a"
// 最高点 统计信息
let highStat = StatisticModel(title: "24h High", value: high)
// 24 小时内最低点
let low = coinModel.low24H?.asCurrencyWith6Decimals() ?? "n/a"
// 最低点 统计信息
let lowStat = StatisticModel(title: "24h Low", value: low)
// 24 小时内价格变化
let priceChange = coinModel.priceChange24H?.asCurrencyWith6Decimals() ?? "n/a"
// 当前交易货币 24 小时的价格变化百分比
let pricePercentChange2 = coinModel.priceChangePercentage24H
// 24 小时内价格变化 统计信息
let priceChangeStat = StatisticModel(title: "24h Price Change", value: priceChange, percentageChange: pricePercentChange2)
// 24 小时内市值变化值
let marketCapChange = "$" + (coinModel.marketCapChange24H?.formattedWithAbbreviations() ?? "")
// 市值 24 小时变化百分比
let marketCapPercentChange2 = coinModel.marketCapChangePercentage24H
// 24 小时内市值变换 统计信息
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 blockTimeStat = StatisticModel(title: "Block Time", value: blockTimeString)
// 哈希/散列 算法
let hashing = coinDetailModel?.hashingAlgorithm ?? "n/a"
let hashingStat = StatisticModel(title: "Hashing Algorithm", value: hashing)
// 当前货币附加信息
return [highStat, lowStat, priceChangeStat, marketCapChangeStat, blockTimeStat, hashingStat]
}
}
quot; + (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 blockTimeStat = StatisticModel(title: "Block Time", value: blockTimeString)
let hashing = coinDetailModel?.hashingAlgorithm ?? "n/a"
let hashingStat = StatisticModel(title: "Hashing Algorithm", value: hashing)
return [highStat, lowStat, priceChangeStat, marketCapChangeStat, blockTimeStat, hashingStat]
}
}
4. 货币详情 View/视图 层
4.1 创建折线视图 ChartView.swift
import SwiftUI
struct ChartView: View {
private let data: [Double]
private let maxY: Double
private let minY: Double
private let lineColor: Color
private let startingDate: Date
private let endingDate: Date
@State private var percentage: CGFloat = 0
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
endingDate = Date(coinGeckoString: coin.lastUpdated ?? "")
startingDate = endingDate.addingTimeInterval(-7 * 24 * 60 * 60)
}
var body: some View {
VStack {
chartView
.frame(height: 200)
.background(chartBackground)
.overlay(chartYAxis.padding(.horizontal, 4), alignment: .leading)
chartDateLabels
.padding(.horizontal, 4)
}
.font(.caption)
.foregroundColor(Color.theme.secondaryText)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation(.linear(duration: 2.0)) {
percentage = 1.0
}
}
}
}
}
struct ChartView_Previews: PreviewProvider {
static var previews: some View {
ChartView(coin: dev.coin)
}
}
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))
}
}
.trim(from: 0, to: percentage)
.stroke(lineColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
.shadow(color: lineColor, radius: 10, x: 0.0, y: 10)
.shadow(color: lineColor.opacity(0.5), radius: 10, x: 0.0, y: 20)
.shadow(color: lineColor.opacity(0.2), radius: 10, x: 0.0, y: 30)
.shadow(color: lineColor.opacity(0.1), radius: 10, x: 0.0, y: 40)
}
}
private var chartBackground: some View{
VStack{
Divider()
Spacer()
Divider()
Spacer()
Divider()
}
}
private var chartYAxis: some View{
VStack{
Text(maxY.formattedWithAbbreviations())
Spacer()
Text(((maxY + minY) * 0.5).formattedWithAbbreviations())
Spacer()
Text(minY.formattedWithAbbreviations())
}
}
private var chartDateLabels: some View{
HStack {
Text(startingDate.asShortDateString())
Spacer()
Text(endingDate.asShortDateString())
}
}
}
4.2 创建货币详情视图,DetailView.swift
import SwiftUI
struct DetailLoadingView: View{
@Binding var coin: CoinModel?
var body: some View {
ZStack {
if let coin = coin{
DetailView(coin: coin)
}
}
}
}
struct DetailView: View {
@StateObject private var viewModel: DetailViewModel
@State private var showFullDescription: Bool = false
private let colums: [GridItem] = [
GridItem(.flexible()),
GridItem(.flexible())
]
private let spacing: CGFloat = 30
init(coin: CoinModel) {
_viewModel = StateObject(wrappedValue: DetailViewModel(coin: coin))
}
var body: some View {
ScrollView {
VStack {
ChartView(coin: viewModel.coin)
.padding(.vertical)
VStack(spacing: 20) {
overviewTitle
Divider()
descriptionSection
overviewGrid
additionalTitle
Divider()
additionalGrid
websiteSection
}
.padding()
}
}
.background(Color.theme.background.ignoresSafeArea())
.navigationTitle(viewModel.coin.name)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
navigationBarTrailing
}
}
}
}
extension DetailView{
private var navigationBarTrailing: some View{
HStack {
Text(viewModel.coin.symbol.uppercased())
.font(.headline)
.foregroundColor(Color.theme.secondaryText)
CoinImageView(coin: viewModel.coin)
.frame(width: 25, height: 25)
}
}
private var overviewTitle: some View{
Text("Overview")
.font(.title)
.bold()
.foregroundColor(Color.theme.accent)
.frame(maxWidth: .infinity, alignment: .leading)
}
private var additionalTitle: some View{
Text("Additional Details")
.font(.title)
.bold()
.foregroundColor(Color.theme.accent)
.frame(maxWidth: .infinity, alignment: .leading)
}
private var descriptionSection: some View{
ZStack {
if let description = viewModel.description,
!description.isEmpty {
VStack(alignment: .leading) {
Text(description)
.lineLimit(showFullDescription ? nil : 3)
.font(.callout)
.foregroundColor(Color.theme.secondaryText)
Button {
withAnimation(.easeInOut) {
showFullDescription.toggle()
}
} label: {
Text(showFullDescription ? "Less" : "Read more...")
.font(.caption)
.fontWeight(.bold)
.padding(.vertical, 4)
}
.accentColor(.blue)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
private var overviewGrid: some View{
LazyVGrid(
columns: colums,
alignment: .leading,
spacing: spacing,
pinnedViews: []) {
ForEach(viewModel.overviewStatistics) { stat in
StatisticView(stat:stat)
}
}
}
private var additionalGrid: some View{
LazyVGrid(
columns: colums,
alignment: .leading,
spacing: spacing,
pinnedViews: []) {
ForEach(viewModel.additionalStatistics) { stat in
StatisticView(stat: stat)
}
}
}
private var websiteSection: some View{
VStack(alignment: .leading, spacing: 12){
if let websiteString = viewModel.websiteURL,
let url = URL(string: websiteString){
Link("Website", destination: url)
}
Spacer()
if let redditString = viewModel.redditURL,
let url = URL(string: redditString){
Link("Reddit", destination: url)
}
}
.accentColor(.blue)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.headline)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
DetailView(coin: dev.coin)
}
}
}
5. 效果图:
