• DetailView/货币详情页 的实现


    1. 创建货币详情数据模型类 CoinDetailModel.swift

    1. import Foundation
    2. // JSON Data
    3. /*
    4. URL:
    5. https://api.coingecko.com/api/v3/coins/bitcoin?localization=false&tickers=false&market_data=false&community_data=false&developer_data=false&sparkline=false
    6. Response:
    7. {
    8. "id": "bitcoin",
    9. "symbol": "btc",
    10. "name": "Bitcoin",
    11. "asset_platform_id": null,
    12. "platforms": {
    13. "": ""
    14. },
    15. "detail_platforms": {
    16. "": {
    17. "decimal_place": null,
    18. "contract_address": ""
    19. }
    20. },
    21. "block_time_in_minutes": 10,
    22. "hashing_algorithm": "SHA-256",
    23. "categories": [
    24. "Cryptocurrency",
    25. "Layer 1 (L1)"
    26. ],
    27. "public_notice": null,
    28. "additional_notices": [],
    29. "description": {
    30. "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 SHA-256 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 Litecoin, Peercoin, Primecoin, and so on.\r\n\r\nThe cryptocurrency then took off with the innovation of the turing-complete smart contract by Ethereum which led to the development of other amazing projects such as EOS, Tron, and even crypto-collectibles such as CryptoKitties."
    31. },
    32. "links": {
    33. "homepage": [
    34. "http://www.bitcoin.org",
    35. "",
    36. ""
    37. ],
    38. "blockchain_site": [
    39. "https://blockchair.com/bitcoin/",
    40. "https://btc.com/",
    41. "https://btc.tokenview.io/",
    42. "https://www.oklink.com/btc",
    43. "https://3xpl.com/bitcoin",
    44. "",
    45. "",
    46. "",
    47. "",
    48. ""
    49. ],
    50. "official_forum_url": [
    51. "https://bitcointalk.org/",
    52. "",
    53. ""
    54. ],
    55. "chat_url": [
    56. "",
    57. "",
    58. ""
    59. ],
    60. "announcement_url": [
    61. "",
    62. ""
    63. ],
    64. "twitter_screen_name": "bitcoin",
    65. "facebook_username": "bitcoins",
    66. "bitcointalk_thread_identifier": null,
    67. "telegram_channel_identifier": "",
    68. "subreddit_url": "https://www.reddit.com/r/Bitcoin/",
    69. "repos_url": {
    70. "github": [
    71. "https://github.com/bitcoin/bitcoin",
    72. "https://github.com/bitcoin/bips"
    73. ],
    74. "bitbucket": []
    75. }
    76. },
    77. "image": {
    78. "thumb": "https://assets.coingecko.com/coins/images/1/thumb/bitcoin.png?1547033579",
    79. "small": "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579",
    80. "large": "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579"
    81. },
    82. "country_origin": "",
    83. "genesis_date": "2009-01-03",
    84. "sentiment_votes_up_percentage": 73.21,
    85. "sentiment_votes_down_percentage": 26.79,
    86. "watchlist_portfolio_users": 1326950,
    87. "market_cap_rank": 1,
    88. "coingecko_rank": 1,
    89. "coingecko_score": 83.151,
    90. "developer_score": 99.241,
    91. "community_score": 83.341,
    92. "liquidity_score": 100.011,
    93. "public_interest_score": 0.073,
    94. "public_interest_stats": {
    95. "alexa_rank": 9440,
    96. "bing_matches": null
    97. },
    98. "status_updates": [],
    99. "last_updated": "2023-08-11T08:43:13.856Z"
    100. }
    101. */
    102. /// 交易货币详情模型
    103. struct CoinDetailModel: Codable {
    104. let id, symbol, name: String?
    105. let blockTimeInMinutes: Int?
    106. let hashingAlgorithm: String?
    107. let description: Description?
    108. let links: Links?
    109. enum CodingKeys: String, CodingKey {
    110. case id, symbol, name
    111. case blockTimeInMinutes = "block_time_in_minutes"
    112. case hashingAlgorithm = "hashing_algorithm"
    113. case description, links
    114. }
    115. /// 去掉 HTML 链接的描述
    116. var readableDescription: String? {
    117. return description?.en?.removingHTMLOccurances
    118. }
    119. }
    120. struct Description: Codable {
    121. let en: String?
    122. }
    123. struct Links: Codable {
    124. let homepage: [String]?
    125. let subredditURL: String?
    126. enum CodingKeys: String, CodingKey {
    127. case homepage
    128. case subredditURL = "subreddit_url"
    129. }
    130. }

    2. 创建货币详情数据服务类 CoinDetailDataService.swift

    1. import Foundation
    2. import Combine
    3. /// 交易货币详情服务
    4. class CoinDetailDataService{
    5. // 交易货币详情模型数组 Published: 可以拥有订阅者
    6. @Published var coinDetails: CoinDetailModel? = nil
    7. // 随时取消操作
    8. var coinDetailSubscription: AnyCancellable?
    9. // 传入的货币模型
    10. let coin: CoinModel
    11. init(coin: CoinModel) {
    12. self.coin = coin
    13. getCoinDetails()
    14. }
    15. /// 获取交易硬币详情
    16. func getCoinDetails(){
    17. 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")
    18. else { return }
    19. coinDetailSubscription = NetworkingManager.downLoad(url: url)
    20. .decode(type: CoinDetailModel.self, decoder: JSONDecoder())
    21. .receive(on: DispatchQueue.main)
    22. .sink(receiveCompletion: NetworkingManager.handleCompletion,
    23. receiveValue: { [weak self] returnCoinDetails in
    24. // 解除强引用 (注意)
    25. self?.coinDetails = returnCoinDetails
    26. // 取消订阅者
    27. self?.coinDetailSubscription?.cancel()
    28. })
    29. }
    30. }

    3. 创建货币详情 ViewModel 类,DetailViewModel.swift

    1. import Foundation
    2. import Combine
    3. /// 交易货币详情 ViewModel
    4. class DetailViewModel: ObservableObject {
    5. /// 概述统计模型数组
    6. @Published var overviewStatistics: [StatisticModel] = []
    7. /// 附加统计数据数组
    8. @Published var additionalStatistics: [StatisticModel] = []
    9. /// 货币描述
    10. @Published var description: String? = nil
    11. /// 货币官网网站
    12. @Published var websiteURL: String? = nil
    13. /// 货币社区网站
    14. @Published var redditURL: String? = nil
    15. /// 交易货币模型
    16. @Published var coin: CoinModel
    17. /// 交易货币详情请求服务
    18. private let coinDetailService: CoinDetailDataService
    19. /// 随时取消订阅
    20. private var cancellables = Set<AnyCancellable>()
    21. init(coin: CoinModel) {
    22. self.coin = coin
    23. self.coinDetailService = CoinDetailDataService(coin: coin)
    24. self.addSubscribers()
    25. }
    26. /// 添加订阅者
    27. private func addSubscribers(){
    28. // 订阅货币详情数据
    29. coinDetailService.$coinDetails
    30. .combineLatest($coin)
    31. // 数据的转换
    32. .map(mapDataToStatistics)
    33. .sink {[weak self] returnedArrays in
    34. self?.overviewStatistics = returnedArrays.overview
    35. self?.additionalStatistics = returnedArrays.additional
    36. }
    37. .store(in: &cancellables)
    38. // 订阅货币详情数据
    39. coinDetailService.$coinDetails
    40. .sink {[weak self] returnedCoinDetails in
    41. self?.description = returnedCoinDetails?.readableDescription
    42. self?.websiteURL = returnedCoinDetails?.links?.homepage?.first
    43. self?.redditURL = returnedCoinDetails?.links?.subredditURL
    44. }
    45. .store(in: &cancellables)
    46. }
    47. /// 数据转换为统计信息数据
    48. private func mapDataToStatistics(coinDetailModel: CoinDetailModel?, coinModel: CoinModel) -> (overview: [StatisticModel], additional: [StatisticModel]){
    49. // 概述信息
    50. // 当前货币概述信息
    51. let overviewArray = createOvervierArray(coinModel: coinModel)
    52. // 附加信息
    53. // 当前货币附加信息
    54. let additionalArray = createAdditionalArray(coinModel: coinModel, coinDetailModel: coinDetailModel)
    55. // 返回数组
    56. return (overviewArray, additionalArray)
    57. }
    58. /// 创建概述信息数组
    59. private func createOvervierArray(coinModel: CoinModel) -> [StatisticModel]{
    60. // 当前交易货币价格
    61. let price = coinModel.currentPrice.asCurrencyWith6Decimals()
    62. // 当前交易货币价格 24 小时的变化百分比
    63. let pricePercentChange = coinModel.priceChangePercentage24H
    64. // 当前交易货币价格 统计信息
    65. let priceStat = StatisticModel(title: "Current Price", value: price, percentageChange: pricePercentChange)
    66. // 市值 价格
    67. let marketCap = "$" + (coinModel.marketCap?.formattedWithAbbreviations() ?? "")
    68. // 市值 24 小时变化百分比
    69. let marketCapPercentChange = coinModel.marketCapChangePercentage24H
    70. // 市值 统计信息
    71. let marketCapStat = StatisticModel(title: "Market Capitalization", value: marketCap, percentageChange: marketCapPercentChange)
    72. // 当前交易货币的排名
    73. let rank = "\(coinModel.rank)"
    74. // 当前货币排名 统计信息
    75. let rankStat = StatisticModel(title: "Rank", value: rank)
    76. // 交易总量
    77. let volume = coinModel.totalVolume?.formattedWithAbbreviations() ?? ""
    78. // 交易 统计信息
    79. let volumeStat = StatisticModel(title: "Volume", value: volume)
    80. // 当前货币概述信息
    81. return [priceStat, marketCapStat, rankStat, volumeStat]
    82. }
    83. /// 创建附加信息数组
    84. private func createAdditionalArray(coinModel: CoinModel, coinDetailModel: CoinDetailModel?) -> [StatisticModel]{
    85. // 24 小时内最高点
    86. let high = coinModel.high24H?.asCurrencyWith6Decimals() ?? "n/a"
    87. // 最高点 统计信息
    88. let highStat = StatisticModel(title: "24h High", value: high)
    89. // 24 小时内最低点
    90. let low = coinModel.low24H?.asCurrencyWith6Decimals() ?? "n/a"
    91. // 最低点 统计信息
    92. let lowStat = StatisticModel(title: "24h Low", value: low)
    93. // 24 小时内价格变化
    94. let priceChange = coinModel.priceChange24H?.asCurrencyWith6Decimals() ?? "n/a"
    95. // 当前交易货币 24 小时的价格变化百分比
    96. let pricePercentChange2 = coinModel.priceChangePercentage24H
    97. // 24 小时内价格变化 统计信息
    98. let priceChangeStat = StatisticModel(title: "24h Price Change", value: priceChange, percentageChange: pricePercentChange2)
    99. // 24 小时内市值变化值
    100. let marketCapChange = "$" + (coinModel.marketCapChange24H?.formattedWithAbbreviations() ?? "")
    101. // 市值 24 小时变化百分比
    102. let marketCapPercentChange2 = coinModel.marketCapChangePercentage24H
    103. // 24 小时内市值变换 统计信息
    104. let marketCapChangeStat = StatisticModel(title: "24h Market Cap Change", value: marketCapChange, percentageChange: marketCapPercentChange2)
    105. // 区块时间 (分钟为单位)
    106. let blockTime = coinDetailModel?.blockTimeInMinutes ?? 0
    107. let blockTimeString = blockTime == 0 ? "n/a" : "\(blockTime)"
    108. // 统计信息
    109. let blockTimeStat = StatisticModel(title: "Block Time", value: blockTimeString)
    110. // 哈希/散列 算法
    111. let hashing = coinDetailModel?.hashingAlgorithm ?? "n/a"
    112. let hashingStat = StatisticModel(title: "Hashing Algorithm", value: hashing)
    113. // 当前货币附加信息
    114. return [highStat, lowStat, priceChangeStat, marketCapChangeStat, blockTimeStat, hashingStat]
    115. }
    116. }

    4. 货币详情 View/视图 层

      4.1 创建折线视图 ChartView.swift

    1. import SwiftUI
    2. /// 折线视图
    3. struct ChartView: View {
    4. // 7 天价格中的交易货币数据
    5. private let data: [Double]
    6. // Y 最大值
    7. private let maxY: Double
    8. // Y 最小值
    9. private let minY: Double
    10. // 线条颜色
    11. private let lineColor: Color
    12. // 开始日期
    13. private let startingDate: Date
    14. // 结束日期
    15. private let endingDate: Date
    16. // 绘制折线进度的百分比
    17. @State private var percentage: CGFloat = 0
    18. init(coin: CoinModel) {
    19. data = coin.sparklineIn7D?.price ?? []
    20. maxY = data.max() ?? 0
    21. minY = data.min() ?? 0
    22. // 最后一个价格减去第一个价格,为当前的价格变化量
    23. let priceChange = (data.last ?? 0) - (data.first ?? 0)
    24. // 线条颜色
    25. lineColor = priceChange > 0 ? Color.theme.green : Color.theme.red
    26. // 转换开始结束时间格式
    27. endingDate = Date(coinGeckoString: coin.lastUpdated ?? "")
    28. // 没有返回开始时间,根据他是结束日期的前七天,所以定义为结束时间之前间隔为 -7 天
    29. startingDate = endingDate.addingTimeInterval(-7 * 24 * 60 * 60)
    30. }
    31. // 计算 X 点的位置:
    32. // 300 : Viw 的宽
    33. // 100 : 数据个数
    34. // 3 : 得到的增量 300 / 100
    35. // 1 * 3 = 3 : x 位置 的计算
    36. // 2 * 3 = 6
    37. // 3 * 3 = 9
    38. // 100 * 3 = 300
    39. // 计算 Y 点的位置
    40. // 60,000 -> 最大值
    41. // 50,000 -> 最小值
    42. // 60,000 - 50,000 = 10,000 -> yAxis / Y轴
    43. // 52,000 - data point
    44. // 52,000 - 50,000 = 2,000 / 10,000 = 20%
    45. var body: some View {
    46. VStack {
    47. // 折线视图
    48. chartView
    49. .frame(height: 200)
    50. .background(chartBackground)
    51. .overlay(chartYAxis.padding(.horizontal, 4), alignment: .leading)
    52. // X 轴日期文字
    53. chartDateLabels
    54. .padding(.horizontal, 4)
    55. }
    56. .font(.caption)
    57. .foregroundColor(Color.theme.secondaryText)
    58. .onAppear {
    59. // 线程
    60. DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
    61. // 线程具有动画效果
    62. withAnimation(.linear(duration: 2.0)) {
    63. percentage = 1.0
    64. }
    65. }
    66. }
    67. }
    68. }
    69. struct ChartView_Previews: PreviewProvider {
    70. static var previews: some View {
    71. ChartView(coin: dev.coin)
    72. }
    73. }
    74. extension ChartView{
    75. /// 折线视图
    76. private var chartView: some View{
    77. // GeometryReader: 根据试图大小自动布局页面
    78. GeometryReader{ geometry in
    79. Path { path in
    80. for index in data.indices{
    81. // x 点的位置 宽度 / 总数 * 增量
    82. let xPosition = geometry.size.width / CGFloat(data.count) * CGFloat(index + 1)
    83. // Y 轴
    84. let yAxis = maxY - minY
    85. // y 点的位置
    86. let yPosition = (1 - CGFloat((data[index] - minY) / yAxis)) * geometry.size.height
    87. if index == 0 {
    88. // 移至起始点左上角(0,0)
    89. path.move(to: CGPoint(x: xPosition, y: yPosition))
    90. }
    91. // 添加一条线
    92. path.addLine(to: CGPoint(x: xPosition, y: yPosition))
    93. }
    94. }
    95. // 修剪绘制线条的进度
    96. .trim(from: 0, to: percentage)
    97. .stroke(lineColor, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
    98. .shadow(color: lineColor, radius: 10, x: 0.0, y: 10)
    99. .shadow(color: lineColor.opacity(0.5), radius: 10, x: 0.0, y: 20)
    100. .shadow(color: lineColor.opacity(0.2), radius: 10, x: 0.0, y: 30)
    101. .shadow(color: lineColor.opacity(0.1), radius: 10, x: 0.0, y: 40)
    102. }
    103. }
    104. /// 背景
    105. private var chartBackground: some View{
    106. VStack{
    107. Divider()
    108. Spacer()
    109. Divider()
    110. Spacer()
    111. Divider()
    112. }
    113. }
    114. /// Y 轴坐标点文字
    115. private var chartYAxis: some View{
    116. VStack{
    117. Text(maxY.formattedWithAbbreviations())
    118. Spacer()
    119. Text(((maxY + minY) * 0.5).formattedWithAbbreviations())
    120. Spacer()
    121. Text(minY.formattedWithAbbreviations())
    122. }
    123. }
    124. /// X 轴日期文字
    125. private var chartDateLabels: some View{
    126. HStack {
    127. Text(startingDate.asShortDateString())
    128. Spacer()
    129. Text(endingDate.asShortDateString())
    130. }
    131. }
    132. }

      4.2 创建货币详情视图,DetailView.swift

    1. import SwiftUI
    2. /// 加载交易货币详情页
    3. struct DetailLoadingView: View{
    4. /// 交易货币模型
    5. @Binding var coin: CoinModel?
    6. var body: some View {
    7. ZStack {
    8. if let coin = coin{
    9. DetailView(coin: coin)
    10. }
    11. }
    12. }
    13. }
    14. /// 交易货币详情页
    15. struct DetailView: View {
    16. @StateObject private var viewModel: DetailViewModel
    17. /// 是否展开概述内容
    18. @State private var showFullDescription: Bool = false
    19. // 网格样式
    20. private let colums: [GridItem] = [
    21. // flexible: 自动调整大小
    22. GridItem(.flexible()),
    23. GridItem(.flexible())
    24. ]
    25. // 网格间隔
    26. private let spacing: CGFloat = 30
    27. /// 交易货币模型
    28. init(coin: CoinModel) {
    29. _viewModel = StateObject(wrappedValue: DetailViewModel(coin: coin))
    30. }
    31. var body: some View {
    32. ScrollView {
    33. VStack {
    34. // 交易货币折线图
    35. ChartView(coin: viewModel.coin)
    36. // 间隔
    37. .padding(.vertical)
    38. VStack(spacing: 20) {
    39. // 概述信息标题
    40. overviewTitle
    41. Divider()
    42. // 概述信息内容
    43. descriptionSection
    44. // 概述信息网格 View
    45. overviewGrid
    46. // 附加信息标题
    47. additionalTitle
    48. Divider()
    49. // 附加信息网格 View
    50. additionalGrid
    51. // 网站地址
    52. websiteSection
    53. }
    54. .padding()
    55. }
    56. }
    57. .background(Color.theme.background.ignoresSafeArea())
    58. .navigationTitle(viewModel.coin.name)
    59. .toolbar {
    60. ToolbarItem(placement: .navigationBarTrailing) {
    61. navigationBarTrailing
    62. }
    63. }
    64. }
    65. }
    66. extension DetailView{
    67. /// 导航栏右边 Item,建议使用 Toolbar
    68. private var navigationBarTrailing: some View{
    69. HStack {
    70. Text(viewModel.coin.symbol.uppercased())
    71. .font(.headline)
    72. .foregroundColor(Color.theme.secondaryText)
    73. CoinImageView(coin: viewModel.coin)
    74. .frame(width: 25, height: 25)
    75. }
    76. }
    77. /// 概述信息标题
    78. private var overviewTitle: some View{
    79. Text("Overview")
    80. .font(.title)
    81. .bold()
    82. .foregroundColor(Color.theme.accent)
    83. .frame(maxWidth: .infinity, alignment: .leading)
    84. }
    85. /// 附加信息标题
    86. private var additionalTitle: some View{
    87. Text("Additional Details")
    88. .font(.title)
    89. .bold()
    90. .foregroundColor(Color.theme.accent)
    91. .frame(maxWidth: .infinity, alignment: .leading)
    92. }
    93. /// 概述信息内容
    94. private var descriptionSection: some View{
    95. ZStack {
    96. if let description = viewModel.description,
    97. !description.isEmpty {
    98. VStack(alignment: .leading) {
    99. // 描述文本
    100. Text(description)
    101. .lineLimit(showFullDescription ? nil : 3)
    102. .font(.callout)
    103. .foregroundColor(Color.theme.secondaryText)
    104. // 更多按钮
    105. Button {
    106. withAnimation(.easeInOut) {
    107. showFullDescription.toggle()
    108. }
    109. } label: {
    110. Text(showFullDescription ? "Less" : "Read more...")
    111. .font(.caption)
    112. .fontWeight(.bold)
    113. .padding(.vertical, 4)
    114. }
    115. .accentColor(.blue)
    116. }
    117. .frame(maxWidth: .infinity, alignment: .leading)
    118. }
    119. }
    120. }
    121. /// 概述信息网格 View
    122. private var overviewGrid: some View{
    123. LazyVGrid(
    124. columns: colums,
    125. alignment: .leading,
    126. spacing: spacing,
    127. pinnedViews: []) {
    128. ForEach(viewModel.overviewStatistics) { stat in
    129. StatisticView(stat:stat)
    130. }
    131. }
    132. }
    133. /// 附加信息网格 View
    134. private var additionalGrid: some View{
    135. LazyVGrid(
    136. columns: colums,
    137. alignment: .leading,
    138. spacing: spacing,
    139. pinnedViews: []) {
    140. ForEach(viewModel.additionalStatistics) { stat in
    141. StatisticView(stat: stat)
    142. }
    143. }
    144. }
    145. /// 网站地址
    146. private var websiteSection: some View{
    147. VStack(alignment: .leading, spacing: 12){
    148. // 官方网站
    149. if let websiteString = viewModel.websiteURL,
    150. let url = URL(string: websiteString){
    151. Link("Website", destination: url)
    152. }
    153. Spacer()
    154. // 论坛网站
    155. if let redditString = viewModel.redditURL,
    156. let url = URL(string: redditString){
    157. Link("Reddit", destination: url)
    158. }
    159. }
    160. .accentColor(.blue)
    161. .frame(maxWidth: .infinity, alignment: .leading)
    162. .font(.headline)
    163. }
    164. }
    165. struct DetailView_Previews: PreviewProvider {
    166. static var previews: some View {
    167. NavigationView {
    168. DetailView(coin: dev.coin)
    169. }
    170. }
    171. }

    5. 效果图:

  • 相关阅读:
    Mysql数据库基础
    SpringMVC数据格式化
    I2C通信协议
    Python 使用Scapy构造特殊数据包
    【Leetcode】拿捏链表(一)——206.反转链表、203.移除链表元素
    C++标准模板(STL)- 类型支持 (定宽整数类型)(INT8_C,INTMAX_C,UINT8_C,UINTMAX_C,格式化宏常量)
    谷歌浏览器无法翻译已解决
    国产大模型各自优势如何?大家都怎么选?
    谈谈 Kubernetes Operator
    docker harbor 私有仓库
  • 原文地址:https://blog.csdn.net/u011193452/article/details/133763195