From 9a33a4f447f96ba61c3b34ac17026864f3e23e86 Mon Sep 17 00:00:00 2001 From: EA Date: Wed, 22 May 2024 16:45:36 +0600 Subject: [PATCH] Initial implementation of market ETF module --- .../project.pbxproj | 28 ++- .../UnstoppableWallet/Extensions/Etf.swift | 48 +++++ .../Modules/Market/Etf/MarketEtfView.swift | 197 ++++++++++++++++++ .../Market/Etf/MarketEtfViewModel.swift | 124 +++++++++++ .../Modules/Market/MarketGlobalView.swift | 9 +- .../MetricChart/MetricChartViewModel.swift | 16 +- .../UserInterface/SwiftUI/ThemeList.swift | 31 ++- .../en.lproj/Localizable.strings | 8 + 8 files changed, 447 insertions(+), 14 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Extensions/Etf.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfViewModel.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 31540e6271..082916c4c1 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -2980,6 +2980,12 @@ D3384D0A2BFCB43800515664 /* MarketWatchlistSignalsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D082BFCB43800515664 /* MarketWatchlistSignalsView.swift */; }; D3384D0C2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D0B2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift */; }; D3384D0D2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D0B2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift */; }; + D3384D102BFDCBDE00515664 /* MarketEtfView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D0F2BFDCBDE00515664 /* MarketEtfView.swift */; }; + D3384D112BFDCBDE00515664 /* MarketEtfView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D0F2BFDCBDE00515664 /* MarketEtfView.swift */; }; + D3384D132BFDCBE700515664 /* MarketEtfViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D122BFDCBE700515664 /* MarketEtfViewModel.swift */; }; + D3384D142BFDCBE700515664 /* MarketEtfViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D122BFDCBE700515664 /* MarketEtfViewModel.swift */; }; + D3384D162BFDEF6800515664 /* Etf.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D152BFDEF6800515664 /* Etf.swift */; }; + D3384D172BFDEF6800515664 /* Etf.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3384D152BFDEF6800515664 /* Etf.swift */; }; D339A93D29126D0F00B895BE /* HsCryptoKit in Frameworks */ = {isa = PBXBuildFile; productRef = D339A93C29126D0F00B895BE /* HsCryptoKit */; }; D339A93F29126D2A00B895BE /* HsCryptoKit in Frameworks */ = {isa = PBXBuildFile; productRef = D339A93E29126D2A00B895BE /* HsCryptoKit */; }; D3402AEE2BF5D58B003BF6F8 /* WatchlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3402AED2BF5D58B003BF6F8 /* WatchlistViewModel.swift */; }; @@ -4907,6 +4913,9 @@ D3373DB120C52F640082BC4A /* LaunchScreen.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LaunchScreen.xib; sourceTree = ""; }; D3384D082BFCB43800515664 /* MarketWatchlistSignalsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketWatchlistSignalsView.swift; sourceTree = ""; }; D3384D0B2BFCB8F000515664 /* MarketWatchlistSignalBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketWatchlistSignalBadge.swift; sourceTree = ""; }; + D3384D0F2BFDCBDE00515664 /* MarketEtfView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketEtfView.swift; sourceTree = ""; }; + D3384D122BFDCBE700515664 /* MarketEtfViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketEtfViewModel.swift; sourceTree = ""; }; + D3384D152BFDEF6800515664 /* Etf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Etf.swift; sourceTree = ""; }; D3402AED2BF5D58B003BF6F8 /* WatchlistViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistViewModel.swift; sourceTree = ""; }; D3402AF02BF5D59D003BF6F8 /* WatchlistModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistModifier.swift; sourceTree = ""; }; D3402AF62BF71C11003BF6F8 /* WatchlistManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistManager.swift; sourceTree = ""; }; @@ -5542,6 +5551,7 @@ D3833AE92BEE4CAA00ACECFB /* TopPlatform.swift */, D3833AF72BF2181800ACECFB /* MarketPair.swift */, D086A9152BF4D08400462024 /* SendParameters.swift */, + D3384D152BFDEF6800515664 /* Etf.swift */, ); path = Extensions; sourceTree = ""; @@ -7744,6 +7754,7 @@ 58AAA9EB9618EBC895D0B123 /* Market */ = { isa = PBXGroup; children = ( + D3384D0E2BFDCBD100515664 /* Etf */, D3833AFA2BF335B800ACECFB /* News */, D3833AF02BF20B7200ACECFB /* Pairs */, D3833AEC2BF1F0AC00ACECFB /* Platform */, @@ -9319,6 +9330,15 @@ path = UnstoppableWallet; sourceTree = ""; }; + D3384D0E2BFDCBD100515664 /* Etf */ = { + isa = PBXGroup; + children = ( + D3384D0F2BFDCBDE00515664 /* MarketEtfView.swift */, + D3384D122BFDCBE700515664 /* MarketEtfViewModel.swift */, + ); + path = Etf; + sourceTree = ""; + }; D36E50882BF7656E00C361BD /* Watchlist */ = { isa = PBXGroup; children = ( @@ -10753,6 +10773,7 @@ 11B353D3A4F2305366835086 /* NftActivityHeaderView.swift in Sources */, 11B350D00FA0A18EF540C945 /* BottomSingleSelectorViewController.swift in Sources */, 11B359F1AB3B0B00DD42E61C /* TokenProtocol.swift in Sources */, + D3384D112BFDCBDE00515664 /* MarketEtfView.swift in Sources */, 11B3588C582E45D149BB42BB /* Token.swift in Sources */, 11B35FC689D745FFBB3684C4 /* TokenType.swift in Sources */, 11B354F237E59C24ED8F3759 /* TokenQuery.swift in Sources */, @@ -10822,6 +10843,7 @@ 11B352B8015606DD9D48A092 /* CoinAnalyticsHoldersCell.swift in Sources */, ABC9AFA89983A9BCB78E4575 /* Contact.swift in Sources */, ABC9A57DE6436FB8795F50E4 /* ContactBookViewController.swift in Sources */, + D3384D142BFDCBE700515664 /* MarketEtfViewModel.swift in Sources */, ABC9ACE1EDEA27A054EDC2C4 /* ContactBookService.swift in Sources */, ABC9A3510E5BE401AD04DA98 /* ContactBookViewModel.swift in Sources */, ABC9AE042D6A3D70CA64F959 /* ContactBookModule.swift in Sources */, @@ -11011,6 +11033,7 @@ ABC9AD46AE6B5F432E0D2085 /* WalletTokenBalanceViewModel.swift in Sources */, ABC9A69264C2086E4B3B09D2 /* WalletTokenBalanceService.swift in Sources */, ABC9A2A6C3A1EFDD33D53287 /* WalletTokenBalanceModule.swift in Sources */, + D3384D172BFDEF6800515664 /* Etf.swift in Sources */, D313698A2BEA188D00BA6B5B /* ZcashPreSendHandler.swift in Sources */, ABC9A55470228BD7B1535B9B /* WalletTokenBalanceViewItemFactory.swift in Sources */, ABC9ABD7DA0C144C545EE228 /* WalletTokenBalanceCell.swift in Sources */, @@ -12318,6 +12341,7 @@ 11B35B7D8E3DA75CFD13E1FF /* ReservoirNftProvider.swift in Sources */, 11B3519760CB3D8D8C97F689 /* NftContractMetadata.swift in Sources */, 11B352C452D8E9C00FD26E8A /* NftActivityHeaderView.swift in Sources */, + D3384D102BFDCBDE00515664 /* MarketEtfView.swift in Sources */, 11B3589541E87A032D9D2D50 /* BottomSingleSelectorViewController.swift in Sources */, 11B359752E118A95F4705B95 /* TokenProtocol.swift in Sources */, 11B35DDD77B56489D1EB72C5 /* Token.swift in Sources */, @@ -12387,6 +12411,7 @@ 11B35F3F123BFF155DA7F417 /* CoinAnalyticsHoldersCell.swift in Sources */, ABC9A06BE632BD33E5CA4106 /* Contact.swift in Sources */, ABC9A2AA80535822D8731DA4 /* ContactBookViewController.swift in Sources */, + D3384D132BFDCBE700515664 /* MarketEtfViewModel.swift in Sources */, ABC9ADD2EA3745F828763EB4 /* ContactBookService.swift in Sources */, ABC9AD6C3EE6EDD0FB3D623A /* ContactBookViewModel.swift in Sources */, ABC9A05D9F96BE464CFC90CC /* ContactBookModule.swift in Sources */, @@ -12576,6 +12601,7 @@ ABC9AFAB3BB4A1D2BFD4283B /* DataSourceChain.swift in Sources */, ABC9A30A4C740609D0898809 /* WalletTokenBalanceDataSource.swift in Sources */, ABC9A427B3166B8A0630EC8A /* WalletTokenBalanceViewModel.swift in Sources */, + D3384D162BFDEF6800515664 /* Etf.swift in Sources */, D31369892BEA188D00BA6B5B /* ZcashPreSendHandler.swift in Sources */, ABC9A2692A01293B1229EF50 /* WalletTokenBalanceService.swift in Sources */, ABC9AC8ACB374C9B96F05B3C /* WalletTokenBalanceModule.swift in Sources */, @@ -13819,7 +13845,7 @@ repositoryURL = "https://github.com/horizontalsystems/MarketKit.Swift"; requirement = { kind = exactVersion; - version = 3.0.4; + version = 3.0.5; }; }; D3604E7D28F03C1D0066C366 /* XCRemoteSwiftPackageReference "Chart" */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/Extensions/Etf.swift b/UnstoppableWallet/UnstoppableWallet/Extensions/Etf.swift new file mode 100644 index 0000000000..cbf41240f7 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Extensions/Etf.swift @@ -0,0 +1,48 @@ +import Foundation +import MarketKit + +extension Etf: Hashable { + public static func == (lhs: Etf, rhs: Etf) -> Bool { + lhs.ticker == rhs.ticker + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ticker) + } +} + +extension Etf { + func inflow(timePeriod: MarketEtfViewModel.TimePeriod) -> Decimal? { + switch timePeriod { + case let .period(timePeriod): return inflows[timePeriod] + case .all: return totalInflow + } + } +} + +extension [Etf] { + func sorted(sortBy: MarketEtfViewModel.SortBy, timePeriod: MarketEtfViewModel.TimePeriod) -> [Etf] { + sorted { lhsEtf, rhsEtf in + switch sortBy { + case .highestAssets, .lowestAssets: + guard let lhsAssets = lhsEtf.totalAssets else { + return false + } + guard let rhsAssets = rhsEtf.totalAssets else { + return true + } + + return sortBy == .highestAssets ? lhsAssets > rhsAssets : lhsAssets < rhsAssets + case .inflow, .outflow: + guard let lhsInflow = lhsEtf.inflow(timePeriod: timePeriod) else { + return false + } + guard let rhsInflow = rhsEtf.inflow(timePeriod: timePeriod) else { + return true + } + + return sortBy == .inflow ? lhsInflow > rhsInflow : lhsInflow < rhsInflow + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift new file mode 100644 index 0000000000..dee414e461 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift @@ -0,0 +1,197 @@ +import Kingfisher +import MarketKit +import SwiftUI + +struct MarketEtfView: View { + @StateObject var viewModel: MarketEtfViewModel + @StateObject var chartViewModel: MetricChartViewModel + @Binding var isPresented: Bool + + @State private var sortBySelectorPresented = false + @State private var timePeriodSelectorPresented = false + + init(isPresented: Binding) { + _viewModel = StateObject(wrappedValue: MarketEtfViewModel()) + _chartViewModel = StateObject(wrappedValue: MetricChartViewModel.instance(type: .totalMarketCap)) + _isPresented = isPresented + } + + var body: some View { + ThemeNavigationView { + ThemeView { + switch viewModel.state { + case .loading: + VStack(spacing: 0) { + header() + chart() + listHeader(disabled: true) + loadingList() + } + case let .loaded(etfs): + ThemeLazyList { + header() + chart() + list(etfs: etfs) + } + .themeListStyle(.transparent) + case .failed: + VStack(spacing: 0) { + header() + chart() + + SyncErrorView { + viewModel.sync() + } + } + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("button.close".localized) { + isPresented = false + } + } + } + } + } + + @ViewBuilder private func header() -> some View { + HStack(spacing: .margin32) { + VStack(spacing: .margin8) { + Text("market.etf.title".localized).themeHeadline1() + Text("market.etf.description".localized).themeSubhead2() + } + .padding(.vertical, .margin12) + + KFImage.url(URL(string: "https://cdn.blocksdecoded.com/category-icons/lending@3x.png")) + .resizable() + .frame(width: 76, height: 108) + } + .padding(.leading, .margin16) + } + + @ViewBuilder private func chart() -> some View { + ChartView(viewModel: chartViewModel, configuration: .marketCapChart) + .frame(maxWidth: .infinity) + .onFirstAppear { + chartViewModel.start() + } + } + + @ViewBuilder private func listHeader(disabled: Bool = false) -> some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Button(action: { + sortBySelectorPresented = true + }) { + Text(viewModel.sortBy.title) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + + Button(action: { + timePeriodSelectorPresented = true + }) { + Text(viewModel.timePeriod.shortTitle) + } + .buttonStyle(SecondaryButtonStyle(style: .default, rightAccessory: .dropDown)) + .disabled(disabled) + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin8) + } + .alert( + isPresented: $sortBySelectorPresented, + title: "market.sort_by.title".localized, + viewItems: MarketEtfViewModel.SortBy.allCases.map { .init(text: $0.title, selected: viewModel.sortBy == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.sortBy = MarketEtfViewModel.SortBy.allCases[index] + } + ) + .alert( + isPresented: $timePeriodSelectorPresented, + title: "market.time_period.title".localized, + viewItems: viewModel.timePeriods.map { .init(text: $0.title, selected: viewModel.timePeriod == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.timePeriod = viewModel.timePeriods[index] + } + ) + } + + @ViewBuilder private func list(etfs: [Etf]) -> some View { + Section { + ThemeLazyListSectionContent(items: etfs) { etf in + ListRow { + itemContent( + imageUrl: nil, + ticker: etf.ticker, + name: etf.name, + totalAssets: etf.totalAssets, + change: etf.inflow(timePeriod: viewModel.timePeriod) + ) + } + } + } header: { + listHeader().background(Color.themeTyler) + } + } + + @ViewBuilder private func loadingList() -> some View { + ThemeList(items: Array(0 ... 10)) { index in + ListRow { + itemContent( + imageUrl: nil, + ticker: "ABCD", + name: "Ticker Name", + totalAssets: 123_345_678, + change: index % 2 == 0 ? 123_456 : -123_456 + ) + .redacted() + } + } + .themeListStyle(.transparent) + .simultaneousGesture(DragGesture(minimumDistance: 0), including: .all) + } + + @ViewBuilder private func itemContent(imageUrl: URL?, ticker: String, name: String, totalAssets: Decimal?, change: Decimal?) -> some View { + KFImage.url(imageUrl) + .resizable() + .placeholder { RoundedRectangle(cornerRadius: .cornerRadius8).fill(Color.themeSteel20) } + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius8)) + .frame(width: .iconSize32, height: .iconSize32) + + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(ticker).textBody() + Spacer() + Text(totalAssets.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) } ?? "n/a".localized).textBody() + } + + HStack(spacing: .margin8) { + Text(name).textSubhead2() + Spacer() + + if let change, let formatted = ValueFormatter.instance.formatShort(currency: viewModel.currency, value: change) { + if change == 0 { + Text(formatted).textSubhead2() + } else if change > 0 { + Text("+\(formatted)").textSubhead2(color: .themeRemus) + } else { + Text("-\(formatted)").textSubhead2(color: .themeLucian) + } + } else { + Text("----").textSubhead2() + } + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfViewModel.swift new file mode 100644 index 0000000000..84fda69391 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfViewModel.swift @@ -0,0 +1,124 @@ +import Combine +import Foundation +import HsExtensions +import MarketKit + +class MarketEtfViewModel: ObservableObject { + private let marketKit = App.shared.marketKit + private let currencyManager = App.shared.currencyManager + + private var cancellables = Set() + private var tasks = Set() + + private var internalState: State = .loading { + didSet { + syncState() + } + } + + @Published var state: State = .loading + + var sortBy: SortBy = .highestAssets { + didSet { + syncState() + } + } + + var timePeriod: TimePeriod = .period(timePeriod: .day1) { + didSet { + syncState() + } + } + + init() { + currencyManager.$baseCurrency + .sink { [weak self] _ in + self?.sync() + } + .store(in: &cancellables) + + sync() + } + + private func syncState() { + switch internalState { + case .loading: + state = .loading + case let .loaded(etfs): + state = .loaded(etfs: etfs.sorted(sortBy: sortBy, timePeriod: timePeriod)) + case let .failed(error): + state = .failed(error: error) + } + } +} + +extension MarketEtfViewModel { + var currency: Currency { + currencyManager.baseCurrency + } + + var timePeriods: [TimePeriod] { + [.period(timePeriod: .day1), .period(timePeriod: .week1), .period(timePeriod: .month1), .period(timePeriod: .month3), .all] + } + + func sync() { + tasks = Set() + + if case .failed = internalState { + internalState = .loading + } + + Task { [weak self, marketKit, currency] in + do { + let etfs = try await marketKit.etfs(currencyCode: currency.code) + + await MainActor.run { [weak self] in + self?.internalState = .loaded(etfs: etfs) + } + } catch { + await MainActor.run { [weak self] in + self?.internalState = .failed(error: error) + } + } + } + .store(in: &tasks) + } +} + +extension MarketEtfViewModel { + enum State { + case loading + case loaded(etfs: [Etf]) + case failed(error: Error) + } + + enum SortBy: String, CaseIterable { + case highestAssets = "highest_assets" + case lowestAssets = "lowest_assets" + case inflow + case outflow + + var title: String { + "market.etf.sort_by.\(rawValue)".localized + } + } + + enum TimePeriod: Equatable { + case period(timePeriod: HsTimePeriod) + case all + + var title: String { + switch self { + case let .period(timePeriod): return timePeriod.title + case .all: return "market.etf.period.all".localized + } + } + + var shortTitle: String { + switch self { + case let .period(timePeriod): return timePeriod.shortTitle + case .all: return "market.etf.period.all".localized + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalView.swift index a9485f1759..35ae771e10 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalView.swift @@ -5,6 +5,7 @@ struct MarketGlobalView: View { @ObservedObject var viewModel: MarketGlobalViewModel @State private var presentedGlobalMarketMetricsType: MarketGlobalModule.MetricsType? + @State private var etfPresented = false var body: some View { VStack(spacing: 0) { @@ -32,6 +33,9 @@ struct MarketGlobalView: View { .sheet(item: $presentedGlobalMarketMetricsType) { metricsType in MarketGlobalMetricsView(metricsType: metricsType).ignoresSafeArea() } + .sheet(isPresented: $etfPresented) { + MarketEtfView(isPresented: $etfPresented) + } } @ViewBuilder private func content(globalMarketData: MarketGlobalViewModel.GlobalMarketData?) -> some View { @@ -55,7 +59,10 @@ struct MarketGlobalView: View { .padding(.horizontal, .margin8) .padding(.vertical, .margin16) .onTapGesture { - presentedGlobalMarketMetricsType = metricsType + switch metricsType { + case .defiCap: etfPresented = true + default: presentedGlobalMarketMetricsType = metricsType + } } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift index b62bb31349..7c600e4e5a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift @@ -7,7 +7,7 @@ import RxCocoa import RxRelay import RxSwift -class MetricChartViewModel { +class MetricChartViewModel: ObservableObject { private let service: MetricChartService private let factory: MetricChartFactory private var cancellables = Set() @@ -139,3 +139,17 @@ extension MetricChartViewModel: IChartViewTouchDelegate { pointSelectedItemRelay.accept(nil) } } + +extension MetricChartViewModel { + static func instance(type: MarketGlobalModule.MetricsType) -> MetricChartViewModel { + let fetcher = MarketGlobalFetcher(currencyManager: App.shared.currencyManager, marketKit: App.shared.marketKit, metricsType: type) + let service = MetricChartService( + chartFetcher: fetcher, + interval: .byPeriod(.day1), + statPage: type.statPage + ) + + let factory = MetricChartFactory(currentLocale: LanguageManager.shared.currentLocale) + return MetricChartViewModel(service: service, factory: factory) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift index a28676cfa6..b8f1887492 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift @@ -80,17 +80,7 @@ struct ThemeLazyListSection: View { Text("todo") case .transparent: Section { - ForEach(items, id: \.self) { item in - VStack(spacing: 0) { - if items.first == item { - HorizontalDivider() - } - - itemContent(item) - - HorizontalDivider() - } - } + ThemeLazyListSectionContent(items: items, itemContent: itemContent) } header: { Text(header) .themeSubhead1(alignment: .leading) @@ -103,3 +93,22 @@ struct ThemeLazyListSection: View { } } } + +struct ThemeLazyListSectionContent: View { + let items: [Item] + @ViewBuilder let itemContent: (Item) -> Content + + var body: some View { + ForEach(items, id: \.self) { item in + VStack(spacing: 0) { + if items.first == item { + HorizontalDivider() + } + + itemContent(item) + + HorizontalDivider() + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index f00da80854..9b6027e61f 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -901,6 +901,14 @@ "market.global.tvl_in_defi.multi_chain" = "Multi-Chain"; "market.global.tvl_in_defi.filter_by_chain" = "Filter by chain"; +"market.etf.title" = "Total Net Inflow"; +"market.etf.description" = "The net inflow of an ETF equals its cash inflows minus outflows."; +"market.etf.sort_by.highest_assets" = "Highest Assets"; +"market.etf.sort_by.lowest_assets" = "Lowest Assets"; +"market.etf.sort_by.inflow" = "Inflow"; +"market.etf.sort_by.outflow" = "Outflow"; +"market.etf.period.all" = "All"; + // Coin Page "coin_page.overview" = "Price";