diff --git a/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj b/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj index d6e4af9bc..28192a56a 100644 --- a/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj +++ b/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 020EB697299D36AD00E8026B /* dydxApiStatusWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020EB696299D36AD00E8026B /* dydxApiStatusWorker.swift */; }; 0216441128F36FBE00C7093E /* CandleDataPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0216441028F36FBE00C7093E /* CandleDataPoint.swift */; }; 021B68B12AD9B86600C5C3BF /* dydxSecurityViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 021B68A42AD9B86600C5C3BF /* dydxSecurityViewPresenter.swift */; }; + 02279D5F2CE7ECDC00266956 /* SortAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02279D5E2CE7ECD900266956 /* SortAction.swift */; }; + 02279D612CE7ED0400266956 /* FilterAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02279D602CE7ECF400266956 /* FilterAction.swift */; }; 02282E752AC8860300BC9F01 /* dydxOrderbookGroupViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02282E742AC8860300BC9F01 /* dydxOrderbookGroupViewPresenter.swift */; }; 0230376F28C11BE600412B72 /* dydxMarketsViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0230376E28C11BE600412B72 /* dydxMarketsViewBuilder.swift */; }; 0236F0CB2968793A00EB995F /* dydxPortfolioFillsViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0236F0CA2968793A00EB995F /* dydxPortfolioFillsViewPresenter.swift */; }; @@ -55,8 +57,6 @@ 0253179729C1270700D6CC9B /* dydxTradingNetworkViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0253179629C1270700D6CC9B /* dydxTradingNetworkViewBuilder.swift */; }; 0256F54229AFFCAC00A083C0 /* dydxOnboardConnectViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0256F54129AFFCAC00A083C0 /* dydxOnboardConnectViewBuilder.swift */; }; 0257C78E2A00485500F6160B /* SparklineDataPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0257C78D2A00485500F6160B /* SparklineDataPoint.swift */; }; - 025841F228EE9D7C007338D3 /* dydxMarketAssetListViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025841F128EE9D7C007338D3 /* dydxMarketAssetListViewPresenter.swift */; }; - 025841F428EE9DE8007338D3 /* dydxAssetItemChartViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025841F328EE9DE8007338D3 /* dydxAssetItemChartViewPresenter.swift */; }; 0258BA23299294BF0098E1BE /* dydxProfileViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0258BA22299294BF0098E1BE /* dydxProfileViewBuilder.swift */; }; 0258BA2929929E870098E1BE /* dydxProfileButtonsViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0258BA2829929E870098E1BE /* dydxProfileButtonsViewPresenter.swift */; }; 025D22D628F65E1B00C4ADAE /* dydxMarketStatsViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025D22D528F65E1B00C4ADAE /* dydxMarketStatsViewPresenter.swift */; }; @@ -412,6 +412,8 @@ 020EB696299D36AD00E8026B /* dydxApiStatusWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxApiStatusWorker.swift; sourceTree = ""; }; 0216441028F36FBE00C7093E /* CandleDataPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CandleDataPoint.swift; sourceTree = ""; }; 021B68A42AD9B86600C5C3BF /* dydxSecurityViewPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxSecurityViewPresenter.swift; sourceTree = ""; }; + 02279D5E2CE7ECD900266956 /* SortAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortAction.swift; sourceTree = ""; }; + 02279D602CE7ECF400266956 /* FilterAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterAction.swift; sourceTree = ""; }; 02282E742AC8860300BC9F01 /* dydxOrderbookGroupViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxOrderbookGroupViewPresenter.swift; sourceTree = ""; }; 0230376E28C11BE600412B72 /* dydxMarketsViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxMarketsViewBuilder.swift; sourceTree = ""; }; 0236F0CA2968793A00EB995F /* dydxPortfolioFillsViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxPortfolioFillsViewPresenter.swift; sourceTree = ""; }; @@ -452,8 +454,6 @@ 0253179629C1270700D6CC9B /* dydxTradingNetworkViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTradingNetworkViewBuilder.swift; sourceTree = ""; }; 0256F54129AFFCAC00A083C0 /* dydxOnboardConnectViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxOnboardConnectViewBuilder.swift; sourceTree = ""; }; 0257C78D2A00485500F6160B /* SparklineDataPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparklineDataPoint.swift; sourceTree = ""; }; - 025841F128EE9D7C007338D3 /* dydxMarketAssetListViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxMarketAssetListViewPresenter.swift; sourceTree = ""; }; - 025841F328EE9DE8007338D3 /* dydxAssetItemChartViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxAssetItemChartViewPresenter.swift; sourceTree = ""; }; 0258BA22299294BF0098E1BE /* dydxProfileViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxProfileViewBuilder.swift; sourceTree = ""; }; 0258BA2829929E870098E1BE /* dydxProfileButtonsViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxProfileButtonsViewPresenter.swift; sourceTree = ""; }; 025D22D528F65E1B00C4ADAE /* dydxMarketStatsViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxMarketStatsViewPresenter.swift; sourceTree = ""; }; @@ -954,8 +954,8 @@ 025841E128EE9D41007338D3 /* Components */ = { isa = PBXGroup; children = ( - 025841F128EE9D7C007338D3 /* dydxMarketAssetListViewPresenter.swift */, - 025841F328EE9DE8007338D3 /* dydxAssetItemChartViewPresenter.swift */, + 02279D5E2CE7ECD900266956 /* SortAction.swift */, + 02279D602CE7ECF400266956 /* FilterAction.swift */, ); path = Components; sourceTree = ""; @@ -1220,8 +1220,8 @@ 02B8419F28EF68E400C4D25B /* MarketInfo */ = { isa = PBXGroup; children = ( - 277987502BA33F15006DC5CD /* dydxSelectedMarketStore.swift */, 0274B34328F113FD005AF69E /* Components */, + 277987502BA33F15006DC5CD /* dydxSelectedMarketStore.swift */, 02B841B128EF6C6400C4D25B /* dydxMarketInfoViewBuilder.swift */, 277DB22E2C669A6800964F9B /* dydxPredictionMarketsNoticeViewBuilder.swift */, ); @@ -2081,7 +2081,6 @@ 2784D93E2CADE38B00AC03EF /* Utilities+Ext.swift in Sources */, 277E90192B1EA3C3005CCBCB /* dydxRewardsSummaryPresenter.swift in Sources */, 026FC3E42AFACA3300C52F20 /* dydxPresentersBundleClass.swift in Sources */, - 025841F228EE9D7C007338D3 /* dydxMarketAssetListViewPresenter.swift in Sources */, 02F95A8E2A1D6AAD00828F9A /* dydxProfileHistoryViewPresenter.swift in Sources */, 02E90C5A29D62719004E2311 /* dydxFeatureFlagsViewBuilder.swift in Sources */, 6453AB26299D98110041A0C4 /* dydxClosePositionInputEditPresenter.swift in Sources */, @@ -2091,13 +2090,13 @@ 27823CF42C77E21A009BCD51 /* dydxVaultDepositWithdrawViewBuilder.swift in Sources */, 64A4DB9929664818008D8E20 /* dydxTradeReceiptPresenter.swift in Sources */, 0236F0CB2968793A00EB995F /* dydxPortfolioFillsViewPresenter.swift in Sources */, - 025841F428EE9DE8007338D3 /* dydxAssetItemChartViewPresenter.swift in Sources */, 278EA5662BB61C24007A0E17 /* KeyValueStoreProtocolStore+Ext.swift in Sources */, 02860A9F29C15E760079E644 /* dydxOnboardScanViewBuilder.swift in Sources */, 277E90332B1FAE9A005CCBCB /* dydxRewardsHelpViewPresenter.swift in Sources */, 02FAFA5C29D4E08E001A0903 /* dydxDebugViewBuilder.swift in Sources */, 02F543B22C17ABBB000924E4 /* dydxGasTokenViewBuilder.swift in Sources */, 27592DFA2C9A54D6002FBD4B /* dydxTransferAlertsProvider.swift in Sources */, + 02279D612CE7ED0400266956 /* FilterAction.swift in Sources */, 02F700FE29EA0FD9004DEB5E /* dydxReceiptPresenter.swift in Sources */, 27457F5B2C8AC8B700873640 /* dydxVaultDepositWithdrawConfirmationViewBuilder.swift in Sources */, 027E1EF829CA27CD0098666F /* dydxSettingsLandingViewBuilder.swift in Sources */, @@ -2145,6 +2144,7 @@ 272C639C2CBE253000323933 /* VaultNetworking.swift in Sources */, 02031F142AC374150069E00D /* dydxTradeSheetTipBuySellViewPresenter.swift in Sources */, 02B6CE752A7087A700C5F088 /* dydxTransferSubaccountWorker.swift in Sources */, + 02279D5F2CE7ECDC00266956 /* SortAction.swift in Sources */, 02FF0BD229AE92FE00781EDA /* dydxWalletListViewBuilder.swift in Sources */, 027F3F062AB93B3700602E5B /* dydxProfileBalancesViewPresenter.swift in Sources */, 02F6E71F2A8293270018F00C /* dydxProfileFeesViewPresenter.swift in Sources */, diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketConfigsViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketConfigsViewPresenter.swift index 7c624d58f..23d7d0a18 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketConfigsViewPresenter.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketConfigsViewPresenter.swift @@ -33,15 +33,12 @@ class dydxMarketConfigsViewPresenter: HostedViewPresenter Bool) + + static func == (lhs: FilterAction, rhs: FilterAction) -> Bool { + lhs.type == rhs.type + } +} + +enum MarketFiltering: Equatable { + case all + case favorited + case predictionMarkets + case layer1 + case layer2 + case defi + case new + case ai + case nft + case gaming + case meme + case rwa + case ent +} diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/SortAction.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/SortAction.swift new file mode 100644 index 000000000..2e955f535 --- /dev/null +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/SortAction.swift @@ -0,0 +1,79 @@ +// +// SortAction.swift +// dydxPresenters +// +// Created by Rui Huang on 15/11/2024. +// + +import Utilities +import PlatformUI +import Abacus +import dydxStateManager +import dydxFormatter + +// MARK: Sorting + +struct SortAction: Equatable { + static var defaultAction: SortAction { + SortAction(type: .volume24h, + text: DataLocalizer.localize(path: "APP.TRADE.VOLUME"), + action: { first, second in + first.perpetual?.volume24H?.doubleValue ?? 0 > second.perpetual?.volume24H?.doubleValue ?? 0 + }) + } + + static var actions: [SortAction] { + [ + .defaultAction, + SortAction(type: .gainers, + text: DataLocalizer.localize(path: "APP.GENERAL.GAINERS"), + action: { first, second in + first.priceChange24HPercent?.doubleValue ?? 0 > second.priceChange24HPercent?.doubleValue ?? 0 + }), + + SortAction(type: .losers, + text: DataLocalizer.localize(path: "APP.GENERAL.LOSERS"), + action: { first, second in + first.priceChange24HPercent?.doubleValue ?? 0 < second.priceChange24HPercent?.doubleValue ?? 0 + }), + + SortAction(type: .fundingRate, + text: DataLocalizer.localize(path: "APP.GENERAL.FUNDING_RATE_CHART_SHORT"), + action: { first, second in + first.perpetual?.nextFundingRate?.doubleValue ?? 0 > second.perpetual?.nextFundingRate?.doubleValue ?? 0 + }), + + SortAction(type: .name, + text: DataLocalizer.localize(path: "APP.GENERAL.NAME"), + action: { first, second in + first.market ?? "" < second.market ?? "" + }), + + SortAction(type: .price, + text: DataLocalizer.localize(path: "APP.GENERAL.PRICE"), + action: { first, second in + first.oraclePrice?.doubleValue ?? 0 > second.oraclePrice?.doubleValue ?? 0 + }) + ] + } + + private let type: MarketSorting + let text: String + let action: ((PerpetualMarket, PerpetualMarket) -> Bool) + + static func == (lhs: SortAction, rhs: SortAction) -> Bool { + lhs.type == rhs.type + } +} + +enum MarketSorting: Equatable { + case name + case marketCap + case volume24h + case change24h + case openInterest + case fundingRate + case price + case gainers + case losers +} diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/dydxAssetItemChartViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/dydxAssetItemChartViewPresenter.swift deleted file mode 100644 index 8b92f0768..000000000 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/dydxAssetItemChartViewPresenter.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// dydxAssetItemChartViewPresenter.swift -// dydxPresenters -// -// Created by Rui Huang on 10/5/22. -// - -import Utilities -import dydxViews -import PlatformParticles -import ParticlesKit -import Abacus -import dydxStateManager -import Combine -import DGCharts -import PlatformUI -import dydxChart - -// MARK: Asset Item Candle Chart - -protocol dydxAssetItemChartViewPresenterProtocol: HostedViewPresenterProtocol { - var viewModel: dydxChartViewModel? { get } -} - -class dydxAssetItemChartViewPresenter: HostedViewPresenter, dydxAssetItemChartViewPresenterProtocol { - private let lineChartView = LineChartView() - private let listPresenter = LineGraphingListPresenter() - private let lineGraphingPresenter = LineGraphingPresenter() - - @Published var candles: MarketCandles? - @Published var sparklines: [Double]? - @Published var priceChange24HPercent: Double? - - override init() { - super.init() - - viewModel = dydxChartViewModel(chartView: lineChartView) - - lineGraphingPresenter.chartView = lineChartView - lineGraphingPresenter.view = lineChartView - lineGraphingPresenter.presenters = [listPresenter] - } - - override func start() { - super.start() - - Publishers - .CombineLatest( - $sparklines - .compactMap { $0 } - .removeDuplicates(), - $priceChange24HPercent - .compactMap { - ($0 ?? 0) < 0 ? ThemeSettings.negativeColor.uiColor : ThemeSettings.positiveColor.uiColor - } - .removeDuplicates() - ) - .sink { (sparklines: [Double], color: UIColor?) in - self.listPresenter.color = color - self.updateGraphData(sparklines: sparklines) - } - .store(in: &subscriptions) - } - - private func updateGraphData(sparklines: [Double]) { - let dataPoints = sparklines.enumerated() - .map { (index, line) in - SparklineDataPoint(lineValue: line, index: index) - } - - let interactor = ListInteractor() - interactor.list = dataPoints - listPresenter.interactor = interactor - } - - private func updateGraphData(candles: MarketCandles) { - if let candles = candles.candles?[CandleResolution.ONEHOUR.v4Key] { - let candleDataPoints = - Array(candles) - .map { candle in - CandleDataPoint(candle: candle, resolution: CandleResolution.ONEHOUR) - } - let interactor = ListInteractor() - interactor.list = candleDataPoints - listPresenter.interactor = interactor - } - } -} diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/dydxMarketAssetListViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/dydxMarketAssetListViewPresenter.swift deleted file mode 100644 index 6a9312959..000000000 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Components/dydxMarketAssetListViewPresenter.swift +++ /dev/null @@ -1,361 +0,0 @@ -// -// dydxMarketAssetListViewPresenter.swift -// dydxPresenters -// -// Created by Rui Huang on 10/5/22. -// - -import Utilities -import dydxViews -import PlatformParticles -import RoutingKit -import ParticlesKit -import PlatformUI -import Abacus -import dydxStateManager -import Combine -import dydxFormatter - -// MARK: AssetList - -protocol dydxMarketAssetListViewPresenterProtocol: HostedViewPresenterProtocol { - var viewModel: dydxMarketAssetListViewModel? { get } - var selectedSortAction: AnyPublisher? { get set } - var selectedFilterAction: AnyPublisher? { get set } -} - -final class dydxMarketAssetListViewPresenter: HostedViewPresenter, dydxMarketAssetListViewPresenterProtocol { - var selectedSortAction: AnyPublisher? - var selectedFilterAction: AnyPublisher? - - private var chartPresenterMap = [String: dydxAssetItemChartViewPresenter]() - private var favoritePresenterMap = [String: dydxUserFavoriteViewPresenter]() - private var viewModelMap = [String: dydxMarketAssetItemViewModel]() - private let favoriteStore = dydxFavoriteStore() - - override init() { - super.init() - - viewModel = dydxMarketAssetListViewModel() - } - - override func start() { - super.start() - - guard let selectedSortAction = selectedSortAction, let selectedFilterAction = selectedFilterAction else { - assertionFailure("No selectedSortAction or selectedFilterAction") - return - } - - let actionPublisher: AnyPublisher<(SortAction?, FilterAction?)?, Never> = - Publishers - .CombineLatest(selectedSortAction, - selectedFilterAction) - .compactMap { ($0, $1) } - .eraseToAnyPublisher() - - Publishers - .CombineLatest4(AbacusStateManager.shared.state.marketList, - AbacusStateManager.shared.state.candlesMap, - AbacusStateManager.shared.state.assetMap, - actionPublisher) - .sink { [weak self] markets, candlesMap, assetMap, actionPublisher in - var sortedMarkets = markets.filter { $0.status?.canTrade == true } - if let action = actionPublisher?.1?.action { - sortedMarkets = sortedMarkets.filter { market in - action(market, assetMap) - } - } - if let action = actionPublisher?.0?.action { - sortedMarkets.sort(by: action) - } - self?.updateAssetList(markets: sortedMarkets, candlesMap: candlesMap, assetMap: assetMap) - } - .store(in: &subscriptions) - } - - override func stop() { - super.stop() - - chartPresenterMap.values.forEach { presenter in - presenter.stop() - } - chartPresenterMap = [:] - - favoritePresenterMap.values.forEach { presenter in - presenter.stop() - } - favoritePresenterMap = [:] - } - - private func updateAssetList(markets: [PerpetualMarket], candlesMap: [String: MarketCandles], assetMap: [String: Asset]) { - var allAssetIds = Set() - - viewModel?.items = markets.compactMap { (market: PerpetualMarket) -> dydxMarketAssetItemViewModel in - allAssetIds.insert(market.assetId) - - let vm = viewModelMap[market.id] ?? dydxMarketAssetItemViewModel() - viewModelMap[market.id] = vm - - let asset = assetMap[market.assetId] - let viewModel = SharedMarketPresenter.createViewModel(market: market, asset: asset) - if viewModel != vm.sharedMarketViewModel { - vm.sharedMarketViewModel = viewModel - if market.priceChange24HPercent?.doubleValue ?? 0 > 0 { - vm.gradientType = .plus - } else if market.priceChange24HPercent?.doubleValue ?? 0 < 0 { - vm.gradientType = .minus - } else { - vm.gradientType = .none - } - } - - vm.onTap = { - Router.shared?.navigate(to: RoutingRequest(path: "/market", params: ["market": market.id]), animated: true, completion: nil) - } - - vm.onFavoriteTap = { - vm.favoriteViewModel?.onTapped?() - } - - // Reuse existing chart presenters - let chartPresenter: dydxAssetItemChartViewPresenter - - if let p = chartPresenterMap[market.assetId] { - chartPresenter = p - } else { - chartPresenter = dydxAssetItemChartViewPresenter() - chartPresenter.start() - chartPresenterMap[market.assetId] = chartPresenter - } - - if candlesMap[market.id] != chartPresenter.candles { - chartPresenter.candles = candlesMap[market.id] - } - chartPresenter.sparklines = market.perpetual?.line?.map(\.doubleValue) - if chartPresenter.priceChange24HPercent != market.priceChange24HPercent?.doubleValue { - chartPresenter.priceChange24HPercent = market.priceChange24HPercent?.doubleValue - } - chartPresenter.$viewModel.assign(to: &vm.$chartViewModel) - - let favoritePresenter: dydxUserFavoriteViewPresenter - if let p = favoritePresenterMap[market.assetId] { - favoritePresenter = p - } else { - favoritePresenter = dydxUserFavoriteViewPresenter(handleTap: false) - favoritePresenter.marketId = market.id - favoritePresenter.start() - favoritePresenterMap[market.assetId] = favoritePresenter - } - - var publisher = vm.$favoriteViewModel - favoritePresenter.$viewModel.assign(to: &publisher) - - var publisher2 = vm.$isFavorited - favoritePresenter.viewModel?.$isFavorited.assign(to: &publisher2) - - return vm - } - - for market in markets { - if allAssetIds.contains(market.assetId) == false { - chartPresenterMap[market.assetId]?.stop() - chartPresenterMap.removeValue(forKey: market.assetId) - } - } - } -} - -// MARK: Sorting - -struct SortAction: Equatable { - static var defaultAction: SortAction { - SortAction(type: .volume24h, - text: DataLocalizer.localize(path: "APP.TRADE.VOLUME"), - action: { first, second in - first.perpetual?.volume24H?.doubleValue ?? 0 > second.perpetual?.volume24H?.doubleValue ?? 0 - }) - } - - static var actions: [SortAction] { - [ - .defaultAction, - SortAction(type: .gainers, - text: DataLocalizer.localize(path: "APP.GENERAL.GAINERS"), - action: { first, second in - first.priceChange24HPercent?.doubleValue ?? 0 > second.priceChange24HPercent?.doubleValue ?? 0 - }), - - SortAction(type: .losers, - text: DataLocalizer.localize(path: "APP.GENERAL.LOSERS"), - action: { first, second in - first.priceChange24HPercent?.doubleValue ?? 0 < second.priceChange24HPercent?.doubleValue ?? 0 - }), - - SortAction(type: .fundingRate, - text: DataLocalizer.localize(path: "APP.GENERAL.FUNDING_RATE_CHART_SHORT"), - action: { first, second in - first.perpetual?.nextFundingRate?.doubleValue ?? 0 > second.perpetual?.nextFundingRate?.doubleValue ?? 0 - }), - - SortAction(type: .name, - text: DataLocalizer.localize(path: "APP.GENERAL.NAME"), - action: { first, second in - first.market ?? "" < second.market ?? "" - }), - - SortAction(type: .price, - text: DataLocalizer.localize(path: "APP.GENERAL.PRICE"), - action: { first, second in - first.oraclePrice?.doubleValue ?? 0 > second.oraclePrice?.doubleValue ?? 0 - }) - ] - } - - private let type: MarketSorting - let text: String - let action: ((PerpetualMarket, PerpetualMarket) -> Bool) - - static func == (lhs: SortAction, rhs: SortAction) -> Bool { - lhs.type == rhs.type - } -} - -// MARK: Filter - -struct FilterAction: Equatable { - static var defaultAction: FilterAction { - FilterAction(type: .all, - content: .text(DataLocalizer.localize(path: "APP.GENERAL.ALL")), - action: { _, _ in - true // included - }) - } - - static var actions: [FilterAction] { - var actions = [ - .defaultAction, - - FilterAction(type: .favorited, - content: .icon(UIImage.named("action_like_unselected", bundles: Bundle.particles) ?? UIImage()), - action: { market, _ in - dydxFavoriteStore().isFavorite(marketId: market.id) - }), - - FilterAction(type: .new, - content: .text(DataLocalizer.localize(path: "APP.GENERAL.RECENTLY_LISTED")), - action: { market, _ in - market.perpetual?.isNew ?? false - }) - ] - if dydxBoolFeatureFlag.showPredictionMarketsUI.isEnabled { - let predictionMarketText = DataLocalizer.localize(path: "APP.GENERAL.PREDICTION_MARKET") - let newPillConfig = TabItemViewModel.TabItemContent.PillConfig(text: DataLocalizer.localize(path: "APP.GENERAL.NEW"), - textColor: .colorPurple, - backgroundColor: .colorFadedPurple) - let content = TabItemViewModel.TabItemContent.textWithPillAccessory(text: predictionMarketText, - pillConfig: newPillConfig) - let predictionMarketsAction = FilterAction(type: .predictionMarkets, - content: content, - action: { market, assetMap in - assetMap[market.assetId]?.tags?.contains("Prediction Market") ?? false - }) - actions.append(predictionMarketsAction) - } - actions.append(contentsOf: [ - FilterAction(type: .layer1, - content: .text(DataLocalizer.localize(path: "APP.GENERAL.LAYER_1")), - action: { market, assetMap in - assetMap[market.assetId]?.tags?.contains("Layer 1") ?? false - }), - - FilterAction(type: .layer2, - content: .text(DataLocalizer.localize(path: "APP.GENERAL.LAYER_2")), - action: { market, assetMap in - assetMap[market.assetId]?.tags?.contains("Layer 2") ?? false - }), - - FilterAction(type: .defi, - content: .text(DataLocalizer.localize(path: "APP.GENERAL.DEFI")), - action: { market, assetMap in - assetMap[market.assetId]?.tags?.contains("Defi") ?? false - }), - - FilterAction(type: .ai, - content: .text(DataLocalizer.localize(path: "APP.GENERAL.AI")), - action: { market, assetMap in - if market.assetId == "RENDER" { - print(assetMap[market.assetId]?.tags?.first ?? "aa") - } - return assetMap[market.assetId]?.tags?.contains("AI") ?? false - }), - - FilterAction(type: .nft, - content: .text(DataLocalizer.localize(path: "APP.GENERAL.NFT")), - action: { market, assetMap in - assetMap[market.assetId]?.tags?.contains("NFT") ?? false - }), - - FilterAction(type: .gaming, - content: .text(DataLocalizer.localize(path: "APP.GENERAL.GAMING")), - action: { market, assetMap in - assetMap[market.assetId]?.tags?.contains("Gaming") ?? false - }), - - FilterAction(type: .meme, - content: .text(DataLocalizer.localize(path: "APP.GENERAL.MEME")), - action: { market, assetMap in - assetMap[market.assetId]?.tags?.contains("Meme") ?? false - }), - - FilterAction(type: .rwa, - content: .text(DataLocalizer.localize(path: "APP.GENERAL.REAL_WORLD_ASSET_SHORT")), - action: { market, assetMap in - assetMap[market.assetId]?.tags?.contains("RWA") ?? false - }), - - FilterAction(type: .ent, - content: .text(DataLocalizer.localize(path: "APP.GENERAL.ENTERTAINMENT")), - action: { market, assetMap in - assetMap[market.assetId]?.tags?.contains("ENT") ?? false - }) - ]) - return actions - } - - let type: MarketFiltering - let content: TabItemViewModel.TabItemContent - let action: ((PerpetualMarket, [String: Asset]) -> Bool) - - static func == (lhs: FilterAction, rhs: FilterAction) -> Bool { - lhs.type == rhs.type - } -} - -enum MarketSorting: Equatable { - case name - case marketCap - case volume24h - case change24h - case openInterest - case fundingRate - case price - case gainers - case losers -} - -enum MarketFiltering: Equatable { - case all - case favorited - case predictionMarkets - case layer1 - case layer2 - case defi - case new - case ai - case nft - case gaming - case meme - case rwa - case ent -} diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Search/dydxMarketsSearchViewBuilder.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Search/dydxMarketsSearchViewBuilder.swift index b3f250941..66a98836a 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Search/dydxMarketsSearchViewBuilder.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Search/dydxMarketsSearchViewBuilder.swift @@ -14,33 +14,51 @@ import PlatformUI import dydxStateManager import Abacus import Combine +import dydxFormatter + +private var cachedPresenter: dydxMarketsSearchViewPresenter? public class dydxMarketsSearchViewBuilder: NSObject, ObjectBuilderProtocol { public func build() -> T? { - let presenter = dydxMarketsSearchViewPresenter() + let presenter = cachedPresenter ?? dydxMarketsSearchViewPresenter() + cachedPresenter = presenter let view = presenter.viewModel?.createView() ?? PlatformViewModel().createView() let configuration = HostingViewControllerConfiguration(fixedHeight: UIScreen.main.bounds.height) return dydxMarketsSearchViewController(presenter: presenter, view: view, configuration: configuration) as? T } } -private class dydxMarketsSearchViewController: HostingViewController { +private class dydxMarketsSearchViewController: HostingViewController { override public func arrive(to request: RoutingRequest?, animated: Bool) -> Bool { guard request?.path == "/markets/search" else { return false } if let presenter = presenter as? dydxMarketsSearchViewPresenter { - presenter.shouldShowResultsForEmptySearch = parser.asBoolean(request?.params?["shouldShowResultsForEmptySearch"])?.boolValue ?? false + presenter.shouldShowResultsForEmptySearch = parser.asBoolean(request?.params?["shouldShowResultsForEmptySearch"])?.boolValue ?? true } return true } } -private class dydxMarketsSearchViewPresenter: dydxSearchViewPresenter { - private var chartPresenterMap = [String: dydxAssetItemChartViewPresenter]() +protocol dydxMarketsSearchViewPresenterProtocol: HostedViewPresenterProtocol { + var viewModel: dydxMarketsSearchViewModel? { get } +} +private class dydxMarketsSearchViewPresenter: HostedViewPresenter, dydxMarketsSearchViewPresenterProtocol { fileprivate var shouldShowResultsForEmptySearch: Bool = false override init() { super.init() + + let viewModel = dydxMarketsSearchViewModel() + viewModel.cancelAction = { + Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: true, completion: nil) + } + viewModel.marketsListViewModel?.onTap = { marketViewModel in + Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: true) { _, _ in + Router.shared?.navigate(to: RoutingRequest(path: "/trade", params: ["market": marketViewModel.marketId]), animated: true, completion: nil) + } + } + + self.viewModel = viewModel } override func start() { @@ -51,11 +69,10 @@ private class dydxMarketsSearchViewPresenter: dydxSearchViewPresenter { } Publishers - .CombineLatest4(AbacusStateManager.shared.state.marketList, - AbacusStateManager.shared.state.candlesMap, + .CombineLatest3(AbacusStateManager.shared.state.marketList, AbacusStateManager.shared.state.assetMap, searchTextPublisher.removeDuplicates()) - .sink { [weak self] (markets: [PerpetualMarket], candlesMap: [String: MarketCandles], assetMap: [String: Asset], searchText: String) in + .sink { [weak self] (markets: [PerpetualMarket], assetMap: [String: Asset], searchText: String) in var filterMarkets = markets.filter { market in guard market.status?.canTrade == true, searchText.isNotEmpty || self?.shouldShowResultsForEmptySearch == true, @@ -69,63 +86,15 @@ private class dydxMarketsSearchViewPresenter: dydxSearchViewPresenter { if searchText.isEmpty && self?.shouldShowResultsForEmptySearch == true { filterMarkets.sort { ($0.perpetual?.volume24H?.doubleValue ?? 0) > $1.perpetual?.volume24H?.doubleValue ?? 0 } } - self?.updateAssetList(markets: filterMarkets, candlesMap: candlesMap, assetMap: assetMap) + self?.updateAssetList(markets: filterMarkets, assetMap: assetMap) } .store(in: &subscriptions) } - override func stop() { - super.stop() - - chartPresenterMap.values.forEach { presenter in - presenter.stop() - } - chartPresenterMap = [:] - } - - private func updateAssetList(markets: [PerpetualMarket], candlesMap: [String: MarketCandles], assetMap: [String: Asset]) { - var allAssetIds = Set() - viewModel?.itemList?.items = markets.compactMap { (market: PerpetualMarket) -> dydxMarketAssetItemViewModel in - allAssetIds.insert(market.assetId) - - let vm = dydxMarketAssetItemViewModel() - + private func updateAssetList(markets: [PerpetualMarket], assetMap: [String: Asset]) { + viewModel?.marketsListViewModel?.markets = markets.compactMap { (market: PerpetualMarket) -> dydxMarketViewModel in let asset = assetMap[market.assetId] - let viewModel = SharedMarketPresenter.createViewModel(market: market, asset: asset) - if viewModel != vm.sharedMarketViewModel { - vm.sharedMarketViewModel = viewModel - } - - vm.onTap = { - Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: false) { _, _ in - Router.shared?.navigate(to: RoutingRequest(path: "/trade", params: ["market": market.id]), animated: true, completion: nil) - } - } - - // Reuse existing chart presenters - let chartPresenter: dydxAssetItemChartViewPresenter - if let p = chartPresenterMap[market.assetId] { - chartPresenter = p - } else { - chartPresenter = dydxAssetItemChartViewPresenter() - chartPresenter.start() - chartPresenterMap[market.assetId] = chartPresenter - } - if candlesMap[market.id] != chartPresenter.candles { - chartPresenter.candles = candlesMap[market.id] - } - chartPresenter.sparklines = market.perpetual?.line?.map(\.doubleValue) - if chartPresenter.priceChange24HPercent != market.priceChange24HPercent?.doubleValue { - chartPresenter.priceChange24HPercent = market.priceChange24HPercent?.doubleValue - } - vm.chartViewModel = chartPresenter.viewModel - - return vm - } - - for market in markets where allAssetIds.contains(market.assetId) == false { - chartPresenterMap[market.assetId]?.stop() - chartPresenterMap.removeValue(forKey: market.assetId) + return dydxMarketViewModel.createFrom(market: market, asset: asset) } } } diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Shared/SharedMarketPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Shared/SharedMarketPresenter.swift index 7fe6907a3..9d3607b1c 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Shared/SharedMarketPresenter.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/Shared/SharedMarketPresenter.swift @@ -32,15 +32,13 @@ class SharedMarketPresenter: HostedViewPresenter, SharedM override func start() { super.start() - let marketPublisher = $marketId - .compactMap { $0 } - .flatMap { AbacusStateManager.shared.state.market(of: $0) } - .compactMap { $0 } - Publishers - .CombineLatest(marketPublisher, + .CombineLatest(AbacusStateManager.shared.state.marketMap, AbacusStateManager.shared.state.assetMap) - .sink { [weak self] (market: PerpetualMarket, assetMap: [String: Asset]) in + .sink { [weak self] (marketMap: [String: PerpetualMarket], assetMap: [String: Asset]) in + guard let marketId = self?.marketId, + let market = marketMap[marketId] else { return } + let asset = assetMap[market.assetId] self?.viewModel = SharedMarketPresenter.createViewModel(market: market, asset: asset) } diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift index d4e98991f..16f7cd7f1 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Markets/dydxMarketsViewBuilder.swift @@ -117,16 +117,7 @@ private class dydxMarketsViewPresenter: HostedViewPresenter dydxMarketViewModel { + dydxMarketViewModel(marketId: market.id, + assetId: asset?.displayableAssetId ?? market.assetId, + iconUrl: asset?.resources?.imageUrl, + volume24H: market.perpetual?.volume24H?.doubleValue ?? 0, + sparkline: market.perpetual?.line?.map(\.doubleValue) ?? [], + price: dydxFormatter.shared.dollar(number: market.oraclePrice?.doubleValue, digits: market.configs?.displayTickSizeDecimals?.intValue ?? 2), + change: market.priceChange24HPercent?.doubleValue ?? 0, + isFavorite: dydxFavoriteStore.shared.isFavorite(marketId: market.id) + ) + } +} diff --git a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj index ab7fa0ef1..de3485d57 100644 --- a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj +++ b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj @@ -112,14 +112,13 @@ 0284201429AD705B00C0E7CC /* dydxFormatter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0284200F29AD704E00C0E7CC /* dydxFormatter.framework */; }; 0284201629AD71B600C0E7CC /* Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0284201529AD71B600C0E7CC /* Enums.swift */; }; 02860A9129C15E670079E644 /* dydxOnboardScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02860A8429C15E670079E644 /* dydxOnboardScanView.swift */; }; + 02865DD62CE7C4F100F03BC8 /* dydxMarketsSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02865DD52CE7C4F100F03BC8 /* dydxMarketsSearchView.swift */; }; 028DB33E2A0589270090BE58 /* dydxProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028DB33D2A0589270090BE58 /* dydxProfileHeaderView.swift */; }; 02934CE5290067F1005DB99C /* SideChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02934CE4290067F1005DB99C /* SideChange.swift */; }; 0297A0F72A6109E500619181 /* ProgressStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0297A0F62A6109E500619181 /* ProgressStepView.swift */; }; 029CBE6C28F5F3F600259C1D /* dydxMarketFundingChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029CBE6B28F5F3F600259C1D /* dydxMarketFundingChartView.swift */; }; 029CBE7728F608F400259C1D /* dydxMarketTradesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029CBE7628F608F400259C1D /* dydxMarketTradesView.swift */; }; 02A5C858297FBCBE00FFE1F9 /* dydxTransferView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A5C857297FBCBE00FFE1F9 /* dydxTransferView.swift */; }; - 02A8975E28E6A941006F1658 /* dydxMarketAssetItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A8975D28E6A941006F1658 /* dydxMarketAssetItemView.swift */; }; - 02A8976028E6A962006F1658 /* dydxMarketAssetListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A8975F28E6A962006F1658 /* dydxMarketAssetListView.swift */; }; 02A9B60C29005A3F00AE1516 /* AmountChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9B60B29005A3F00AE1516 /* AmountChange.swift */; }; 02A9B60E29005D5A00AE1516 /* AmountText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9B60D29005D5A00AE1516 /* AmountText.swift */; }; 02A9B6102900605400AE1516 /* SizeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A9B60F2900605400AE1516 /* SizeText.swift */; }; @@ -506,14 +505,13 @@ 0284200929AD704E00C0E7CC /* dydxFormatter.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = dydxFormatter.xcodeproj; path = ../dydxFormatter/dydxFormatter.xcodeproj; sourceTree = ""; }; 0284201529AD71B600C0E7CC /* Enums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Enums.swift; sourceTree = ""; }; 02860A8429C15E670079E644 /* dydxOnboardScanView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxOnboardScanView.swift; sourceTree = ""; }; + 02865DD52CE7C4F100F03BC8 /* dydxMarketsSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxMarketsSearchView.swift; sourceTree = ""; }; 028DB33D2A0589270090BE58 /* dydxProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxProfileHeaderView.swift; sourceTree = ""; }; 02934CE4290067F1005DB99C /* SideChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideChange.swift; sourceTree = ""; }; 0297A0F62A6109E500619181 /* ProgressStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressStepView.swift; sourceTree = ""; }; 029CBE6B28F5F3F600259C1D /* dydxMarketFundingChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxMarketFundingChartView.swift; sourceTree = ""; }; 029CBE7628F608F400259C1D /* dydxMarketTradesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxMarketTradesView.swift; sourceTree = ""; }; 02A5C857297FBCBE00FFE1F9 /* dydxTransferView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTransferView.swift; sourceTree = ""; }; - 02A8975D28E6A941006F1658 /* dydxMarketAssetItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxMarketAssetItemView.swift; sourceTree = ""; }; - 02A8975F28E6A962006F1658 /* dydxMarketAssetListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxMarketAssetListView.swift; sourceTree = ""; }; 02A9B60B29005A3F00AE1516 /* AmountChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountChange.swift; sourceTree = ""; }; 02A9B60D29005D5A00AE1516 /* AmountText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountText.swift; sourceTree = ""; }; 02A9B60F2900605400AE1516 /* SizeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizeText.swift; sourceTree = ""; }; @@ -770,6 +768,7 @@ 0230376B28C11B0E00412B72 /* Markets */ = { isa = PBXGroup; children = ( + 02865DC82CE7C4D200F03BC8 /* Search */, 0230377428C13D8E00412B72 /* Components */, 02D1341428EB563500B46941 /* Shared */, 0230377228C13BBB00412B72 /* dydxMarketsView.swift */, @@ -782,8 +781,6 @@ children = ( 0230377528C13DA500412B72 /* dydxMarketsHeaderView.swift */, 0276B9C728E6477500101791 /* dydxMarketSummaryView.swift */, - 02A8975D28E6A941006F1658 /* dydxMarketAssetItemView.swift */, - 02A8975F28E6A962006F1658 /* dydxMarketAssetListView.swift */, 27E3C3E72CA33673009AEFEC /* dydxMarketListView.swift */, 02D1344028EBA86200B46941 /* dydxMarketAssetSortView.swift */, 02D1344E28ECA75C00B46941 /* dydxMarketAssetFilterView.swift */, @@ -1289,6 +1286,14 @@ path = Scan; sourceTree = ""; }; + 02865DC82CE7C4D200F03BC8 /* Search */ = { + isa = PBXGroup; + children = ( + 02865DD52CE7C4F100F03BC8 /* dydxMarketsSearchView.swift */, + ); + path = Search; + sourceTree = ""; + }; 028AC6EC2A5F78E800FE0891 /* Components */ = { isa = PBXGroup; children = ( @@ -2106,7 +2111,6 @@ 2700A3172C5D72BB00880AFA /* dydxVaultChartViewModel.swift in Sources */, 0256F53629AFFC9800A083C0 /* dydxOnboardConnectView.swift in Sources */, 024F48902965CE9200E40247 /* dydxPortfolioDetailsView.swift in Sources */, - 02A8976028E6A962006F1658 /* dydxMarketAssetListView.swift in Sources */, 02FD2B8B292307A200A5609E /* LeverageRiskChange.swift in Sources */, 2729F9E92C64093700769BA4 /* dydxMarketsBannerViewModel.swift in Sources */, 024F465929646BF000E40247 /* dydxMarketDepthHightlightView.swift in Sources */, @@ -2175,6 +2179,7 @@ 027F3EF72AB93ADC00602E5B /* dydxProfileBalancesViewModel.swift in Sources */, 02404FA82CD42E77001B571D /* dydxVaultTosView.swift in Sources */, 024B44F52983E38D00E35D54 /* dydxTradeStatusLogoView.swift in Sources */, + 02865DD62CE7C4F100F03BC8 /* dydxMarketsSearchView.swift in Sources */, 02AEE4BC28F0DC78006842E8 /* dydxMarketPriceCandlesControlView.swift in Sources */, 0297A0F72A6109E500619181 /* ProgressStepView.swift in Sources */, 023848D52A9E784800B1A673 /* dydxTosView.swift in Sources */, @@ -2261,7 +2266,6 @@ 029CBE6C28F5F3F600259C1D /* dydxMarketFundingChartView.swift in Sources */, 02084C682981253200CF9522 /* dydxValidationView.swift in Sources */, 02934CE5290067F1005DB99C /* SideChange.swift in Sources */, - 02A8975E28E6A941006F1658 /* dydxMarketAssetItemView.swift in Sources */, 02CA62852A7EBDBA006067DB /* dydxPortfolioFeesView.swift in Sources */, 024B7B5628B7F8AB00F7C386 /* dydxThemes.swift in Sources */, 27C6E4C92BC8C30E00ED892A /* dydxCustomLimitPriceViewModel.swift in Sources */, diff --git a/dydx/dydxViews/dydxViews/_v4/MarketInfo/Components/Stats/dydxMarketStatsView.swift b/dydx/dydxViews/dydxViews/_v4/MarketInfo/Components/Stats/dydxMarketStatsView.swift index 7f0c09179..4928f01f4 100644 --- a/dydx/dydxViews/dydxViews/_v4/MarketInfo/Components/Stats/dydxMarketStatsView.swift +++ b/dydx/dydxViews/dydxViews/_v4/MarketInfo/Components/Stats/dydxMarketStatsView.swift @@ -13,7 +13,7 @@ import Utilities public class dydxMarketStatsViewModel: PlatformViewModel { public struct StatItem: Hashable { public static func == (lhs: dydxMarketStatsViewModel.StatItem, rhs: dydxMarketStatsViewModel.StatItem) -> Bool { - lhs.header == rhs.header && lhs.token == rhs.token + false // always reload } public func hash(into hasher: inout Hasher) { @@ -32,7 +32,25 @@ public class dydxMarketStatsViewModel: PlatformViewModel { let token: TokenTextViewModel? } - @Published public var statItems: [StatItem] = [] + @Published private var statRows = [[StatItem]]() + + @Published public var statItems: [StatItem] = [] { + didSet { + var newItems = [[StatItem]]() + var currentRow = [StatItem]() + for item in statItems { + currentRow.append(item) + if currentRow.count == 2 { + newItems.append(currentRow) + currentRow = [] + } + } + if currentRow.count > 0 { + newItems.append(currentRow) + } + statRows = newItems + } + } public init() { } @@ -50,57 +68,43 @@ public class dydxMarketStatsViewModel: PlatformViewModel { PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] style in guard let self = self else { return AnyView(PlatformView.nilView) } - let itemPerRow = 2 - return AnyView( VStack(spacing: 0) { DividerModel().createView(parentStyle: style) - ForEach(0.. PlatformView { PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] style in - guard let self = self, let sharedMarketViewModel = self.sharedMarketViewModel else { + guard let self = self, self.sharedMarketViewModel != nil else { return AnyView(PlatformView.nilView) } - return HStack(spacing: 28) { + return HStack(spacing: 16) { ChevronBackButtonModel(onBackButtonTap: self.onBackButtonTap ?? {}) .createView(parentStyle: style) + .frame(width: 32) self.createMarketSelectorView(parentStyle: parentStyle, styleKey: styleKey) + .frame(maxWidth: .infinity) self.favoriteViewModel?.createView(parentStyle: style) - .viewModel.createView(parentStyle: parentStyle, styleKey: styleKey) - }.wrappedInAnyView() + .frame(width: 32) + } + .frame(maxWidth: .infinity) + .padding(.horizontal, 12) + .wrappedInAnyView() } } } diff --git a/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketAssetItemView.swift b/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketAssetItemView.swift deleted file mode 100644 index 699cfd801..000000000 --- a/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketAssetItemView.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// dydxMarketAssetItemView.swift -// dydxViews -// -// Created by Rui Huang on 9/29/22. -// Copyright © 2022 dYdX Trading Inc. All rights reserved. -// - -import SwiftUI -import PlatformUI -import Utilities -import PlatformParticles -import DGCharts - -// TODO: Add swipe gestures to lazyvstack :/ https://stackoverflow.com/questions/64103113/deleting-rows-inside-lazyvstack-and-foreach-in-swiftui - -open class dydxMarketAssetItemViewModel: PlatformViewModel { - @Published public var sharedMarketViewModel: SharedMarketViewModel? = SharedMarketViewModel() - @Published public var favoriteViewModel: dydxUserFavoriteViewModel? = dydxUserFavoriteViewModel() - @Published public var isFavorited: Bool = false - @Published public var onTap: (() -> Void)? - @Published public var onFavoriteTap: (() -> Void)? - @Published public var chartViewModel: dydxChartViewModel? - @Published public var gradientType: GradientType = .plus - - public init() { } - - public static var previewValue: dydxMarketAssetItemViewModel { - let vm = dydxMarketAssetItemViewModel() - vm.sharedMarketViewModel = SharedMarketViewModel.previewValue - vm.favoriteViewModel = .previewValue - vm.chartViewModel = dydxChartViewModel(chartView: LineChartView()) - return vm - } - - public override func createView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> PlatformView { - PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] style in - guard let self = self else { return AnyView(PlatformView.nilView) } - - var leftCellSwipeAccessory: CellSwipeAccessory? - if let favoriteView = self.favoriteViewModel?.createView(parentStyle: style).tint(ThemeColor.SemanticColor.layer2.color) { - leftCellSwipeAccessory = CellSwipeAccessory(accessoryView: AnyView(favoriteView), action: self.onFavoriteTap) - } - - return AnyView( - self.createContent(parentStyle: style) - .onTapGesture { - self.onTap?() - } - .swipeActions(leftCellSwipeAccessory: leftCellSwipeAccessory, - rightCellSwipeAccessory: nil) - ) - } - } - - private func createContent(parentStyle: ThemeStyle) -> some View { - ZStack { - let icon = PlatformIconViewModel(type: .url(url: sharedMarketViewModel?.logoUrl), - clip: .defaultCircle, - size: CGSize(width: 40, height: 40)) - .createView(parentStyle: parentStyle) - .wrappedViewModel - - let main = createMain(parentStyle: parentStyle) - - let trailing = createTrailing(parentStyle: parentStyle) - - PlatformTableViewCellViewModel(logo: icon, - main: main.wrappedViewModel, - trailing: trailing.wrappedViewModel) - .createView(parentStyle: parentStyle) - .frame(height: 64) - .themeGradient(background: .layer3, gradientType: gradientType) - .cornerRadius(16) - - if isFavorited { - PlatformIconViewModel(type: .asset(name: "action_like_small", bundle: Bundle.dydxView), - size: CGSize(width: 12, height: 12)) - .createView(parentStyle: parentStyle) - .position(x: 24, y: 12) - } - } - } - - private func createMain(parentStyle: ThemeStyle) -> some View { - HStack(spacing: 0) { - HStack { - VStack(alignment: .leading) { - HStack(spacing: 4) { - Text(sharedMarketViewModel?.assetId ?? "") - .themeColor(foreground: .textPrimary) - .themeFont(fontType: .plus, fontSize: .medium) - .layoutPriority(1) - } - Text(sharedMarketViewModel?.volume24H ?? "") - .themeFont(fontType: .base, fontSize: .small) - } - Spacer() - } - .frame(width: 90) - - chartViewModel?.createView(parentStyle: parentStyle) - .frame(width: 72, height: 50) - .padding(.trailing, -16) - // don't allow hit testing since chart views have a bunch of gesture config which interfere with parent gestures - .allowsHitTesting(false) - } - } - - private func createTrailing(parentStyle: ThemeStyle) -> some View { - VStack(alignment: .trailing, spacing: 2) { - Text(sharedMarketViewModel?.indexPrice ?? "") - .themeColor(foreground: .textPrimary) - .themeFont(fontType: .plus, fontSize: .medium) - .minimumScaleFactor(0.5) - if let priceChangePercent24H = sharedMarketViewModel?.priceChangePercent24H { - priceChangePercent24H - .createView(parentStyle: parentStyle, styleKey: "asset_list_item_24h_volume") - } - } - } - -} - -#if DEBUG -struct dydxMarketAssetItemView_Previews_Dark: PreviewProvider { - @StateObject static var themeSettings = ThemeSettings.shared - - static var previews: some View { - ThemeSettings.applyDarkTheme() - ThemeSettings.applyStyles() - return dydxMarketAssetItemViewModel.previewValue - .createView() - // .edgesIgnoringSafeArea(.bottom) - .previewLayout(.sizeThatFits) - } -} - -struct dydxMarketAssetItemView_Previews_Light: PreviewProvider { - @StateObject static var themeSettings = ThemeSettings.shared - - static var previews: some View { - ThemeSettings.applyLightTheme() - ThemeSettings.applyStyles() - return dydxMarketAssetItemViewModel.previewValue - .createView() - // .edgesIgnoringSafeArea(.bottom) - .previewLayout(.sizeThatFits) - } -} -#endif diff --git a/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketAssetListView.swift b/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketAssetListView.swift deleted file mode 100644 index f9cdbf444..000000000 --- a/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketAssetListView.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// dydxMarketAssetListView.swift -// dydxViews -// -// Created by Rui Huang on 9/29/22. -// Copyright © 2022 dYdX Trading Inc. All rights reserved. -// - -import SwiftUI -import PlatformUI -import Utilities - -public class dydxMarketAssetListViewModel: PlatformListViewModel { - public init() { - super.init( - intraItemSeparator: false - ) - } - - public static var previewValue: dydxMarketAssetListViewModel = { - let vm = dydxMarketAssetListViewModel() - vm.items = [ - dydxMarketAssetItemViewModel.previewValue, - dydxMarketAssetItemViewModel.previewValue, - dydxMarketAssetItemViewModel.previewValue - ] - return vm - }() -} - -#if DEBUG -struct dydxMarketAssetListView_Previews_Dark: PreviewProvider { - @StateObject static var themeSettings = ThemeSettings.shared - - static var previews: some View { - ThemeSettings.applyDarkTheme() - ThemeSettings.applyStyles() - return dydxMarketAssetListViewModel.previewValue - .createView() - // .edgesIgnoringSafeArea(.bottom) - .previewLayout(.sizeThatFits) - } -} - -struct dydxMarketAssetListView_Previews_Light: PreviewProvider { - @StateObject static var themeSettings = ThemeSettings.shared - - static var previews: some View { - ThemeSettings.applyLightTheme() - ThemeSettings.applyStyles() - return dydxMarketAssetListViewModel.previewValue - .createView() - // .edgesIgnoringSafeArea(.bottom) - .previewLayout(.sizeThatFits) - } -} -#endif diff --git a/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketListView.swift b/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketListView.swift index e6daa4771..ddbc06b50 100644 --- a/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketListView.swift +++ b/dydx/dydxViews/dydxViews/_v4/Markets/Components/dydxMarketListView.swift @@ -68,7 +68,7 @@ private struct dydxMarketListView: View { } } -public class dydxMarketViewModel: PlatformViewModeling { +public class dydxMarketViewModel: PlatformViewModel { public let marketId: String public let assetId: String public let iconUrl: String? @@ -101,8 +101,12 @@ public class dydxMarketViewModel: PlatformViewModeling { self.isFavorite = isFavorite } - public func createView(parentStyle: ThemeStyle, styleKey: String?) -> some View { - dydxMarketView(viewModel: self) + public override func createView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> PlatformView { + PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] _ in + guard let self = self else { return AnyView(PlatformView.nilView) } + + return dydxMarketView(viewModel: self).wrappedInAnyView() + } } } diff --git a/dydx/dydxViews/dydxViews/_v4/Markets/Search/dydxMarketsSearchView.swift b/dydx/dydxViews/dydxViews/_v4/Markets/Search/dydxMarketsSearchView.swift new file mode 100644 index 000000000..569b4893c --- /dev/null +++ b/dydx/dydxViews/dydxViews/_v4/Markets/Search/dydxMarketsSearchView.swift @@ -0,0 +1,132 @@ +// +// dydxMarketsSearchView.swift +// dydxUI +// +// Created by Rui Huang on 15/11/2024. +// Copyright © 2024 dYdX Trading Inc. All rights reserved. +// + +import SwiftUI +import PlatformUI +import Utilities + +public class dydxMarketsSearchViewModel: PlatformViewModel { + @Published public var searchText: String = "" + @Published public var cancelAction: (() -> Void)? + @Published public var marketsListViewModel: dydxMarketListViewModel? = dydxMarketListViewModel() + + private lazy var searchTextBinding = Binding( + get: { + self.searchText + }, + set: { + self.searchText = $0 + } + ) + + public init() { } + + public static var previewValue: dydxMarketsSearchViewModel { + let vm = dydxMarketsSearchViewModel() + vm.searchText = "Test String" + return vm + } + + public override func createView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> PlatformView { + PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] style in + guard let self = self else { return AnyView(PlatformView.nilView) } + + return AnyView( + VStack { + self.createSearchBar(style: style) + .padding([.leading, .trailing]) + + if (self.marketsListViewModel?.markets.count ?? 0) > 0 { + ScrollView(.vertical, showsIndicators: false) { + LazyVStack(pinnedViews: [.sectionHeaders]) { + Section { + self.marketsListViewModel? + .createView() + .padding(.horizontal, 16) + } + } + } + } else { + HStack { + Spacer() + Text(DataLocalizer.localize(path: "APP.GENERAL.START_SEARCH")) + .themeFont(fontType: .plus, fontSize: .larger) + .themeColor(foreground: .textTertiary) + Spacer() + } + .padding(.top, 120) + } + + Spacer() + } + .padding(.top, 32) + .themeColor(background: .layer2) + .makeSheet() + .ignoresSafeArea(edges: [.bottom]) + ) + } + } + + private func createSearchBar(style: ThemeStyle) -> AnyView { + AnyView( + HStack { + PlatformInputModel(value: self.searchTextBinding, + currentValue: self.searchText, + placeHolder: DataLocalizer.localize(path: "APP.GENERAL.SEARCH"), + keyboardType: .default, + focusedOnAppear: true) + .createView(parentStyle: style) + .frame(height: 40) + .padding(.vertical, 2) + .padding(.horizontal, 12) + .themeColor(background: .layer3) + .clipShape(Capsule()) + + PlatformButtonViewModel(content: PlatformIconViewModel(type: .asset(name: "icon_cancel", bundle: Bundle.dydxView), + clip: .circle(background: .layer3, spacing: 24, borderColor: .layer6), + size: CGSize(width: 42, height: 42)), + type: .iconType, + action: self.cancelAction ?? {}) + .createView(parentStyle: style) + } + .padding(.vertical, 8) + ) + } +} + +#if DEBUG +struct dydxMarketSearchView_Previews_Dark: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + ThemeSettings.applyDarkTheme() + ThemeSettings.applyStyles() + return dydxMarketsSearchViewModel.previewValue + .createView() + .themeColor(background: .layer0) + .environmentObject(themeSettings) + // .edgesIgnoringSafeArea(.bottom) + .previewLayout(.sizeThatFits) + } +} + +struct dydxMarketSearchView_Previews_Light: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + ThemeSettings.applyLightTheme() + ThemeSettings.applyStyles() + return dydxMarketsSearchViewModel.previewValue + .createView() + .themeColor(background: .layer0) + .environmentObject(themeSettings) + // .edgesIgnoringSafeArea(.bottom) + .previewLayout(.sizeThatFits) + } +} +#endif diff --git a/dydx/dydxViews/dydxViews/_v4/Trade/TradeInput/Components/Orderbook/dydxOrderbookSideView.swift b/dydx/dydxViews/dydxViews/_v4/Trade/TradeInput/Components/Orderbook/dydxOrderbookSideView.swift index 2cce5f381..ecea7595a 100644 --- a/dydx/dydxViews/dydxViews/_v4/Trade/TradeInput/Components/Orderbook/dydxOrderbookSideView.swift +++ b/dydx/dydxViews/dydxViews/_v4/Trade/TradeInput/Components/Orderbook/dydxOrderbookSideView.swift @@ -128,7 +128,6 @@ public class dydxOrderbookSideViewModel: PlatformViewModel, Equatable { sideBySideView(line: line) .themeColor(foreground: line.textColor) .padding(.horizontal, 12) - .padding(.vertical, 4) .themeFont(fontType: .number, fontSize: .medium) ) case .topDown: @@ -159,20 +158,36 @@ public class dydxOrderbookSideViewModel: PlatformViewModel, Equatable { } func sideBySideView(line: dydxOrderbookLine) -> any View { - HStack { + HStack(spacing: 8) { let priceText = dydxFormatter.shared.dollar(number: line.price, size: tickSize) Text(priceText ?? "") - Spacer() + .lineLimit(1) + .minimumScaleFactor(0.5) + .leftAligned() + .frame(maxWidth: .infinity) + Text(line.sizeText) + .lineLimit(1) + .minimumScaleFactor(0.5) + .rightAligned() + .frame(maxWidth: .infinity) } } func topDownView(line: dydxOrderbookLine) -> any View { - HStack { + HStack(spacing: 4) { let priceText = dydxFormatter.shared.dollar(number: line.price, size: tickSize) Text(line.sizeText) - Spacer() + .lineLimit(1) + .minimumScaleFactor(0.5) + .leftAligned() + .frame(maxWidth: .infinity) + Text(priceText ?? "") + .lineLimit(1) + .minimumScaleFactor(0.5) + .rightAligned() + .frame(maxWidth: .infinity) } } @@ -243,11 +258,21 @@ public class dydxOrderbookAsksViewModel: dydxOrderbookSideViewModel { } override func sideBySideView(line: dydxOrderbookLine) -> any View { - HStack { + HStack(spacing: 8) { let priceText = dydxFormatter.shared.dollar(number: line.price, size: tickSize) Text(line.sizeText) - Spacer() + .themeFont(fontType: .number, fontSize: .medium) + .lineLimit(1) + .minimumScaleFactor(0.5) + .leftAligned() + .frame(maxWidth: .infinity) + Text(priceText ?? "") + .themeFont(fontType: .number, fontSize: .medium) + .lineLimit(1) + .minimumScaleFactor(0.5) + .rightAligned() + .frame(maxWidth: .infinity) } } }