diff --git a/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj b/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj index 5d4055f2..aef1232e 100644 --- a/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj +++ b/dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 0280B3A629CB63E10017D64A /* dydxOnboardWelcomeViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0280B3A529CB63E10017D64A /* dydxOnboardWelcomeViewBuilder.swift */; }; 0284202F29AD727200C0E7CC /* dydxViews.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0284201F29AD725000C0E7CC /* dydxViews.framework */; }; 02860A9F29C15E760079E644 /* dydxOnboardScanViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02860A9329C15E760079E644 /* dydxOnboardScanViewBuilder.swift */; }; + 028CF14F2D1489E900476930 /* dydxSimpleUIMarketSearchViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028CF1412D1489E800476930 /* dydxSimpleUIMarketSearchViewPresenter.swift */; }; 028DB3402A05893D0090BE58 /* dydxProfileHeaderViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028DB33F2A05893D0090BE58 /* dydxProfileHeaderViewPresenter.swift */; }; 028FB3EC2AD642B30013136C /* dydxTokenConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028FB3EB2AD642B30013136C /* dydxTokenConstants.swift */; }; 0295392329FB256E009026E3 /* dydxThemeViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295392229FB256E009026E3 /* dydxThemeViewBuilder.swift */; }; @@ -104,6 +105,7 @@ 02D6DAFA2D1234B7008AAEA1 /* dydxSimpleUIMarketsViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D6DAF92D1234B4008AAEA1 /* dydxSimpleUIMarketsViewBuilder.swift */; }; 02D6DAFC2D123576008AAEA1 /* dydxRootBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D6DAFB2D123570008AAEA1 /* dydxRootBuilder.swift */; }; 02D6DB272D124AAF008AAEA1 /* dydxAppModeViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D6DB262D124AAD008AAEA1 /* dydxAppModeViewBuilder.swift */; }; + 02D6DBB92D134BEA008AAEA1 /* dydxSimpleUIMarketListViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D6DBB82D134BE8008AAEA1 /* dydxSimpleUIMarketListViewPresenter.swift */; }; 02D9518C2C926B9C007BB2B4 /* dydxNotificationPrimerViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D9518B2C926B9C007BB2B4 /* dydxNotificationPrimerViewPresenter.swift */; }; 02DA6E0D2AD897EE0048126C /* starkex-lib.js in Resources */ = {isa = PBXBuildFile; fileRef = 02DA6DFF2AD897EE0048126C /* starkex-lib.js */; }; 02DA6E0E2AD897EE0048126C /* starkex-eth.js in Resources */ = {isa = PBXBuildFile; fileRef = 02DA6E0C2AD897EE0048126C /* starkex-eth.js */; }; @@ -481,6 +483,7 @@ 0280B3A529CB63E10017D64A /* dydxOnboardWelcomeViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxOnboardWelcomeViewBuilder.swift; sourceTree = ""; }; 0284201929AD725000C0E7CC /* dydxViews.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = dydxViews.xcodeproj; path = ../dydxViews/dydxViews.xcodeproj; sourceTree = ""; }; 02860A9329C15E760079E644 /* dydxOnboardScanViewBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxOnboardScanViewBuilder.swift; sourceTree = ""; }; + 028CF1412D1489E800476930 /* dydxSimpleUIMarketSearchViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSimpleUIMarketSearchViewPresenter.swift; sourceTree = ""; }; 028DB33F2A05893D0090BE58 /* dydxProfileHeaderViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxProfileHeaderViewPresenter.swift; sourceTree = ""; }; 028FB3EB2AD642B30013136C /* dydxTokenConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTokenConstants.swift; sourceTree = ""; }; 0295392229FB256E009026E3 /* dydxThemeViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxThemeViewBuilder.swift; sourceTree = ""; }; @@ -503,6 +506,7 @@ 02D6DAF92D1234B4008AAEA1 /* dydxSimpleUIMarketsViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSimpleUIMarketsViewBuilder.swift; sourceTree = ""; }; 02D6DAFB2D123570008AAEA1 /* dydxRootBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxRootBuilder.swift; sourceTree = ""; }; 02D6DB262D124AAD008AAEA1 /* dydxAppModeViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxAppModeViewBuilder.swift; sourceTree = ""; }; + 02D6DBB82D134BE8008AAEA1 /* dydxSimpleUIMarketListViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSimpleUIMarketListViewPresenter.swift; sourceTree = ""; }; 02D9518B2C926B9C007BB2B4 /* dydxNotificationPrimerViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxNotificationPrimerViewPresenter.swift; sourceTree = ""; }; 02DA6DFF2AD897EE0048126C /* starkex-lib.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "starkex-lib.js"; sourceTree = ""; }; 02DA6E0C2AD897EE0048126C /* starkex-eth.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "starkex-eth.js"; sourceTree = ""; }; @@ -1292,6 +1296,7 @@ 02D6DAF82D1234A8008AAEA1 /* Markets */ = { isa = PBXGroup; children = ( + 02D6DBB72D134BE0008AAEA1 /* components */, 02D6DAF92D1234B4008AAEA1 /* dydxSimpleUIMarketsViewBuilder.swift */, ); path = Markets; @@ -1305,6 +1310,15 @@ path = AppMode; sourceTree = ""; }; + 02D6DBB72D134BE0008AAEA1 /* components */ = { + isa = PBXGroup; + children = ( + 028CF1412D1489E800476930 /* dydxSimpleUIMarketSearchViewPresenter.swift */, + 02D6DBB82D134BE8008AAEA1 /* dydxSimpleUIMarketListViewPresenter.swift */, + ); + path = components; + sourceTree = ""; + }; 02DC87862CD441B500F865C5 /* History */ = { isa = PBXGroup; children = ( @@ -2096,12 +2110,14 @@ 270E7E242A5F6B1B00136793 /* dydxTradeInputSideViewPresenter.swift in Sources */, 278A4D1E2B8EA95A003898EB /* dydxCollectFeedbackActionBuilder.swift in Sources */, 2741E3732A689740000FA190 /* dydxDirectionColorPreferenceViewBuilder.swift in Sources */, + 02D6DBB92D134BEA008AAEA1 /* dydxSimpleUIMarketListViewPresenter.swift in Sources */, 0236F12D296B91BD00EB995F /* dydxPortfolioOrdersViewPresenter.swift in Sources */, 0280B3A629CB63E10017D64A /* dydxOnboardWelcomeViewBuilder.swift in Sources */, 023848D72A9E785900B1A673 /* dydxTosViewBuilder.swift in Sources */, 0238FC4C296DA55A002E1C1A /* dydxOrderDetailsViewBuilder.swift in Sources */, 02F95A7E2A1D314400828F9A /* dydxPortfolioSelectorViewPresenter.swift in Sources */, 020EB697299D36AD00E8026B /* dydxApiStatusWorker.swift in Sources */, + 028CF14F2D1489E900476930 /* dydxSimpleUIMarketSearchViewPresenter.swift in Sources */, 02A5C85E297FBCD700FFE1F9 /* dydxTransferViewBuilder.swift in Sources */, 023848C22A9E637C00B1A673 /* SettingsLandingViewPresenter.swift in Sources */, 0258BA23299294BF0098E1BE /* dydxProfileViewBuilder.swift in Sources */, diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketListViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketListViewPresenter.swift new file mode 100644 index 00000000..5c3c948c --- /dev/null +++ b/dydx/dydxPresenters/dydxPresenters/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketListViewPresenter.swift @@ -0,0 +1,109 @@ +// +// dydxSimpleUIMarketListViewPresenter.swift +// dydxPresenters +// +// Created by Rui Huang on 18/12/2024. +// + +import Utilities +import dydxViews +import PlatformParticles +import RoutingKit +import ParticlesKit +import PlatformUI +import dydxStateManager +import Abacus +import Combine +import dydxFormatter + +protocol dydxSimpleUIMarketListViewPresenterProtocol: HostedViewPresenterProtocol { + var viewModel: dydxSimpleUIMarketListViewModel? { get } +} + +class dydxSimpleUIMarketListViewPresenter: HostedViewPresenter, dydxSimpleUIMarketListViewPresenterProtocol { + + @Published var searchText: String = "" + + override init() { + super.init() + + viewModel = dydxSimpleUIMarketListViewModel() + } + + override func start() { + super.start() + + let searchTextPublisher = $searchText.map({ $0.lowercased() }).removeDuplicates() + + Publishers + .CombineLatest4(AbacusStateManager.shared.state.marketList, + AbacusStateManager.shared.state.assetMap, + AbacusStateManager.shared.state.selectedSubaccountPositions, + searchTextPublisher + ) + .sink { [weak self] markets, assetMap, positions, searchText in + self?.updateMarketList(markets: markets, assetMap: assetMap, positions: positions, searchText: searchText) + } + .store(in: &subscriptions) + } + + private func updateMarketList(markets: [PerpetualMarket], + assetMap: [String: Asset], + positions: [SubaccountPosition], + searchText: String?) { + let markets = markets.filter { $0.status?.canTrade == true } + viewModel?.markets = markets + .compactMap { market in + guard let asset = assetMap[market.assetId] else { + return nil + } + if let searchText = searchText, searchText.isNotEmpty, + asset.displayableAssetId.lowercased().contains(searchText) == false, + asset.name?.lowercased().contains(searchText) == false { + return nil + } + let position = positions.first { position in + position.id == market.id + } + return dydxSimpleUIMarketViewModel.createFrom(market: market, asset: asset, position: position) + } + .sorted { lhs, rhs in + if lhs.leverage != nil && rhs.leverage != nil { + return (lhs.volumn ?? 0) > (rhs.volumn ?? 0) + } else if lhs.leverage != nil { + return true + } else if rhs.leverage != nil { + return false + } + return (lhs.volumn ?? 0) > (rhs.volumn ?? 0) + } + } +} + +private extension dydxSimpleUIMarketViewModel { + static func createFrom(market: PerpetualMarket, asset: Asset?, position: SubaccountPosition?) -> dydxSimpleUIMarketViewModel { + let price = dydxFormatter.shared.dollar(number: market.oraclePrice?.doubleValue, digits: market.configs?.displayTickSizeDecimals?.intValue ?? 2) + let change = SignedAmountViewModel(amount: market.priceChange24HPercent?.doubleValue, + displayType: .percent, + coloringOption: .allText) + var side = SideTextViewModel(side: .custom(DataLocalizer.localize(path: "APP.GENERAL.NO_POSITION"))) + if let position = position { + if position.side.current == Abacus.PositionSide.long_ { + side = SideTextViewModel(side: .long) + } else if position.side.current == Abacus.PositionSide.short_ { + side = SideTextViewModel(side: .short) + } + } + let leverage = dydxFormatter.shared.raw(number: position?.leverage.current?.doubleValue, digits: 3) + return dydxSimpleUIMarketViewModel(marketId: market.id, + assetName: asset?.displayableAssetId ?? market.assetId, + iconUrl: asset?.resources?.imageUrl, + price: price, + change: change, + sideText: side, + leverage: leverage, + volumn: market.perpetual?.volume24HUSDC?.doubleValue, + onMarketSelected: { + }) + } +} diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketSearchViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketSearchViewPresenter.swift new file mode 100644 index 00000000..a13b6976 --- /dev/null +++ b/dydx/dydxPresenters/dydxPresenters/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketSearchViewPresenter.swift @@ -0,0 +1,25 @@ +// +// dydxSimpleUIMarketSearchViewPresenter.swift +// dydxPresenters +// +// Created by Rui Huang on 19/12/2024. +// + +import Utilities +import dydxViews +import PlatformParticles +import RoutingKit +import ParticlesKit +import PlatformUI + +protocol dydxSimpleUIMarketSearchViewPresenterProtocol: HostedViewPresenterProtocol { + var viewModel: dydxSimpleUIMarketSearchViewModel? { get } +} + +class dydxSimpleUIMarketSearchViewPresenter: HostedViewPresenter, dydxSimpleUIMarketSearchViewPresenterProtocol { + override init() { + super.init() + + viewModel = dydxSimpleUIMarketSearchViewModel() + } +} diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/SimpleUI/Markets/dydxSimpleUIMarketsViewBuilder.swift b/dydx/dydxPresenters/dydxPresenters/_v4/SimpleUI/Markets/dydxSimpleUIMarketsViewBuilder.swift index a88558d9..918dc131 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/SimpleUI/Markets/dydxSimpleUIMarketsViewBuilder.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/SimpleUI/Markets/dydxSimpleUIMarketsViewBuilder.swift @@ -35,12 +35,30 @@ public protocol dydxSimpleUIMarketsViewPresenterProtocol: HostedViewPresenterPro public class dydxSimpleUIMarketsViewPresenter: HostedViewPresenter, dydxSimpleUIMarketsViewPresenterProtocol { + private let marketListPresenter = dydxSimpleUIMarketListViewPresenter() + private let marketSearchPresenter = dydxSimpleUIMarketSearchViewPresenter() + + private lazy var childPresenters: [HostedViewPresenterProtocol] = [ + marketListPresenter, + marketSearchPresenter + ] + override init() { - super.init() + let viewModel = dydxSimpleUIMarketsViewModel() + + marketListPresenter.$viewModel.assign(to: &viewModel.$marketList) + marketSearchPresenter.$viewModel.assign(to: &viewModel.$marketSearch) + marketSearchPresenter.viewModel?.$searchText.assign(to: &marketListPresenter.$searchText) + marketSearchPresenter.viewModel?.$focused.assign(to: &viewModel.$keyboardUp) - viewModel = dydxSimpleUIMarketsViewModel() - viewModel?.onSettingTapped = { + viewModel.onSettingTapped = { Router.shared?.navigate(to: RoutingRequest(path: "/settings/app_mode"), animated: true, completion: nil) } + + super.init() + + self.viewModel = viewModel + + attachChildren(workers: childPresenters) } } diff --git a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj index 645a53d0..c6250f1f 100644 --- a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj +++ b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj @@ -113,6 +113,7 @@ 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 */; }; + 028CF1342D1489DD00476930 /* dydxSimpleUIMarketSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028CF1332D1489DD00476930 /* dydxSimpleUIMarketSearchView.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 */; }; @@ -140,6 +141,8 @@ 02D6DAE92D12349A008AAEA1 /* dydxSimpleUIMarketsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D6DAE82D12349A008AAEA1 /* dydxSimpleUIMarketsView.swift */; }; 02D6DB172D124A8C008AAEA1 /* dydxAppModeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D6DB162D124A8C008AAEA1 /* dydxAppModeView.swift */; }; 02D6DBB12D12770D008AAEA1 /* Checkmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D6DBB02D12770A008AAEA1 /* Checkmark.swift */; }; + 02D6DBB42D134723008AAEA1 /* dydxSimpleUIMarketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D6DBB32D134723008AAEA1 /* dydxSimpleUIMarketView.swift */; }; + 02D6DBB62D134BCF008AAEA1 /* dydxSimpleUIMarketListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D6DBB52D134BCF008AAEA1 /* dydxSimpleUIMarketListView.swift */; }; 02D9518A2C926B8E007BB2B4 /* dydxNotificationPrimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D951892C926B8E007BB2B4 /* dydxNotificationPrimerView.swift */; }; 02DC87852CD441A900F865C5 /* dydxVaultHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DC87842CD441A900F865C5 /* dydxVaultHistoryView.swift */; }; 02DC87972CD452BB00F865C5 /* dydxVaultHistoryItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DC87962CD452BB00F865C5 /* dydxVaultHistoryItemView.swift */; }; @@ -510,6 +513,7 @@ 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 = ""; }; + 028CF1332D1489DD00476930 /* dydxSimpleUIMarketSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSimpleUIMarketSearchView.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 = ""; }; @@ -537,6 +541,8 @@ 02D6DAE82D12349A008AAEA1 /* dydxSimpleUIMarketsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSimpleUIMarketsView.swift; sourceTree = ""; }; 02D6DB162D124A8C008AAEA1 /* dydxAppModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxAppModeView.swift; sourceTree = ""; }; 02D6DBB02D12770A008AAEA1 /* Checkmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkmark.swift; sourceTree = ""; }; + 02D6DBB32D134723008AAEA1 /* dydxSimpleUIMarketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSimpleUIMarketView.swift; sourceTree = ""; }; + 02D6DBB52D134BCF008AAEA1 /* dydxSimpleUIMarketListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSimpleUIMarketListView.swift; sourceTree = ""; }; 02D951892C926B8E007BB2B4 /* dydxNotificationPrimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxNotificationPrimerView.swift; sourceTree = ""; }; 02DC87842CD441A900F865C5 /* dydxVaultHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxVaultHistoryView.swift; sourceTree = ""; }; 02DC87962CD452BB00F865C5 /* dydxVaultHistoryItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxVaultHistoryItemView.swift; sourceTree = ""; }; @@ -1437,6 +1443,7 @@ 02D6DAE72D12344E008AAEA1 /* Markets */ = { isa = PBXGroup; children = ( + 02D6DBB22D13470D008AAEA1 /* components */, 02D6DAE82D12349A008AAEA1 /* dydxSimpleUIMarketsView.swift */, ); path = Markets; @@ -1450,6 +1457,16 @@ path = AppMode; sourceTree = ""; }; + 02D6DBB22D13470D008AAEA1 /* components */ = { + isa = PBXGroup; + children = ( + 02D6DBB32D134723008AAEA1 /* dydxSimpleUIMarketView.swift */, + 02D6DBB52D134BCF008AAEA1 /* dydxSimpleUIMarketListView.swift */, + 028CF1332D1489DD00476930 /* dydxSimpleUIMarketSearchView.swift */, + ); + path = components; + sourceTree = ""; + }; 02D9516D2C926A7D007BB2B4 /* Primers */ = { isa = PBXGroup; children = ( @@ -2179,6 +2196,7 @@ 02F99F3E29E4D5750009B3E8 /* dydxTransferSearchItemView.swift in Sources */, 27EB25832C6D1E5E008C187B /* dydxVaultPositionView.swift in Sources */, 27DBF3C92C4A05B9009EB2D6 /* dydxTitledNumberField.swift in Sources */, + 02D6DBB62D134BCF008AAEA1 /* dydxSimpleUIMarketListView.swift in Sources */, 0284201629AD71B600C0E7CC /* Enums.swift in Sources */, 02678FA629666BE600EE346B /* dydxPortfolioOrdersView.swift in Sources */, 273C2F382C496F4F00F8391F /* dydxSliderInputView.swift in Sources */, @@ -2242,6 +2260,7 @@ 2728CE1B2BBCD2AB004C9323 /* dydxGainLossInputViewModel.swift in Sources */, 02AEE36C28EF7CC8006842E8 /* dydxMarketInfoHeaderView.swift in Sources */, 024F488529657F1900E40247 /* dydxPortfolioView.swift in Sources */, + 028CF1342D1489DD00476930 /* dydxSimpleUIMarketSearchView.swift in Sources */, 277E907D2B211553005CCBCB /* dydxRewardsHistoryView.swift in Sources */, 024B79BF28B7F53800F7C386 /* dydxViews.docc in Sources */, 27BAAF112BD851B500F650C3 /* dydxTakeProfitStopLossStatusViewModel.swift in Sources */, @@ -2283,6 +2302,7 @@ 0230A2A328B84A160036DDF3 /* HostingViewController.swift in Sources */, 025BA65A290100CD00CDAE99 /* LeverageRisk.swift in Sources */, 024F487A29657D7900E40247 /* dydxUserFavoriteView.swift in Sources */, + 02D6DBB42D134723008AAEA1 /* dydxSimpleUIMarketView.swift in Sources */, 02B27A3A2AE8BC5A00A995EC /* dydxHelpView.swift in Sources */, 27673BB02C8F971900689E3F /* dydxCheckboxView.swift in Sources */, 02678FAA29666C0E00EE346B /* dydxPortfolioFundingView.swift in Sources */, diff --git a/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketListView.swift b/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketListView.swift new file mode 100644 index 00000000..aa723b63 --- /dev/null +++ b/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketListView.swift @@ -0,0 +1,90 @@ +// +// dydxSimpleUIMarketListView.swift +// dydxUI +// +// Created by Rui Huang on 18/12/2024. +// Copyright © 2024 dYdX Trading Inc. All rights reserved. +// + +import SwiftUI +import PlatformUI +import Utilities + +public class dydxSimpleUIMarketListViewModel: PlatformViewModel { + @Published public var markets: [dydxSimpleUIMarketViewModel]? + + public init() { } + + public static var previewValue: dydxSimpleUIMarketListViewModel { + let vm = dydxSimpleUIMarketListViewModel() + vm.markets = [ + dydxSimpleUIMarketViewModel.previewValue, + dydxSimpleUIMarketViewModel.previewValue + ] + return vm + } + + private let dummyMarket = dydxSimpleUIMarketViewModel(marketId: "_dummy", assetName: "", iconUrl: nil, price: nil, change: nil, sideText: SideTextViewModel.previewValue, leverage: nil, volumn: nil, onMarketSelected: nil) + + 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) } + + // Need to insert "No Market" into ForEach for better performance + let markets: [dydxSimpleUIMarketViewModel] + if self.markets == nil { + markets = [] + } else if self.markets?.count == 0 { + markets = [dummyMarket] + } else { + markets = self.markets ?? [] + } + + let view = ForEach(markets, id: \.marketId) { market in + if market.marketId == "_dummy" { + PlaceholderViewModel(text: DataLocalizer.localize(path: "APP.GENERAL.NO_MARKET")) + .createView(parentStyle: style) + } else { + market.createView(parentStyle: style) + } + if market !== markets.last { + DividerModel().createView(parentStyle: style) + } + } + + return AnyView(view) + } + } +} + +#if DEBUG +struct dydxSimpleUIMarketListView_Previews_Dark: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + ThemeSettings.applyDarkTheme() + ThemeSettings.applyStyles() + return dydxSimpleUIMarketListViewModel.previewValue + .createView() + .themeColor(background: .layer0) + .environmentObject(themeSettings) + // .edgesIgnoringSafeArea(.bottom) + .previewLayout(.sizeThatFits) + } +} + +struct dydxSimpleUIMarketListView_Previews_Light: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + ThemeSettings.applyLightTheme() + ThemeSettings.applyStyles() + return dydxSimpleUIMarketListViewModel.previewValue + .createView() + .themeColor(background: .layer0) + .environmentObject(themeSettings) + // .edgesIgnoringSafeArea(.bottom) + .previewLayout(.sizeThatFits) + } +} +#endif diff --git a/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketSearchView.swift b/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketSearchView.swift new file mode 100644 index 00000000..fae07b47 --- /dev/null +++ b/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketSearchView.swift @@ -0,0 +1,111 @@ +// +// dydxSimpleUIMarketSearchView.swift +// dydxUI +// +// Created by Rui Huang on 19/12/2024. +// Copyright © 2024 dYdX Trading Inc. All rights reserved. +// + +import SwiftUI +import PlatformUI +import Utilities + +public class dydxSimpleUIMarketSearchViewModel: PlatformViewModel { + @Published public var searchText: String = "" + @Published public var cancelAction: (() -> Void)? + @Published public var focused: Bool = false + + private lazy var searchTextBinding = Binding( + get: { + self.searchText + }, + set: { + self.searchText = $0 + } + ) + + public init() { } + + public static var previewValue: dydxSimpleUIMarketSearchViewModel { + let vm = dydxSimpleUIMarketSearchViewModel() + 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) } + + let view = HStack(alignment: .center, spacing: 0) { + PlatformIconViewModel(type: .asset(name: "icon_search", bundle: Bundle.dydxView), + size: CGSize(width: 16, height: 16), + templateColor: .textTertiary) + .createView(parentStyle: style) + .padding(.leading, 16) + + PlatformInputModel(value: self.searchTextBinding, + currentValue: self.searchText, + placeHolder: DataLocalizer.localize(path: "APP.GENERAL.SEARCH"), + keyboardType: .default, + onEditingChanged: { [weak self] focused in + self?.focused = focused + }, + focusedOnAppear: false) + .createView(parentStyle: style) + .frame(height: 40) + .padding(.vertical, 2) + + if self.searchText.isNotEmpty { + let closeIcon = PlatformIconViewModel(type: .asset(name: "icon_cancel", bundle: Bundle.dydxView), + clip: .circle(background: .layer3, spacing: 0, borderColor: .layer6), + size: CGSize(width: 16, height: 16), + templateColor: .textTertiary) + PlatformButtonViewModel(content: closeIcon, + type: .iconType, + action: { [weak self] in + self?.searchText = "" + }) + .createView(parentStyle: style) + .padding(.trailing, 16) + } + } + .themeColor(background: .layer3) + .clipShape(Capsule()) + .padding(.vertical, 8) + .padding(.horizontal, 16) + + return AnyView(view) + } + } +} + +#if DEBUG +struct dydxSimpleUIMarketSearchView_Previews_Dark: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + ThemeSettings.applyDarkTheme() + ThemeSettings.applyStyles() + return dydxSimpleUIMarketSearchViewModel.previewValue + .createView() + .themeColor(background: .layer0) + .environmentObject(themeSettings) + // .edgesIgnoringSafeArea(.bottom) + .previewLayout(.sizeThatFits) + } +} + +struct dydxSimpleUIMarketSearchView_Previews_Light: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + ThemeSettings.applyLightTheme() + ThemeSettings.applyStyles() + return dydxSimpleUIMarketSearchViewModel.previewValue + .createView() + .themeColor(background: .layer0) + .environmentObject(themeSettings) + // .edgesIgnoringSafeArea(.bottom) + .previewLayout(.sizeThatFits) + } +} +#endif diff --git a/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketView.swift b/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketView.swift new file mode 100644 index 00000000..289ce7ec --- /dev/null +++ b/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/components/dydxSimpleUIMarketView.swift @@ -0,0 +1,162 @@ +// +// dydxSimpleUIMarketView.swift +// dydxUI +// +// Created by Rui Huang on 18/12/2024. +// Copyright © 2024 dYdX Trading Inc. All rights reserved. +// + +import SwiftUI +import PlatformUI +import Utilities + +public class dydxSimpleUIMarketViewModel: PlatformViewModel { + public let marketId: String + public let assetName: String + public let iconUrl: String? + public let price: String? + public let change: SignedAmountViewModel? + public let sideText: SideTextViewModel + public let leverage: String? + public let volumn: Double? + public let onMarketSelected: (() -> Void)? + + public init(marketId: String, + assetName: String, + iconUrl: String?, + price: String?, + change: SignedAmountViewModel?, + sideText: SideTextViewModel, + leverage: String?, + volumn: Double?, + onMarketSelected: (() -> Void)? + ) { + self.marketId = marketId + self.assetName = assetName + self.iconUrl = iconUrl + self.price = price + self.change = change + self.sideText = sideText + self.leverage = leverage + self.volumn = volumn + self.onMarketSelected = onMarketSelected + } + + public static var previewValue: dydxSimpleUIMarketViewModel { + let vm = dydxSimpleUIMarketViewModel(marketId: "ETH-USD", + assetName: "ETH", + iconUrl: "https://assets.coingecko.com/coins/images/1/large/bitcoin.png", + price: "50_000", + change: .previewValue, + sideText: .previewValue, + leverage: "1.34", + volumn: nil, + onMarketSelected: nil) + 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) } + + let view = Button { [weak self] in + self?.onMarketSelected?() + } label: { + HStack(spacing: 20) { + HStack(spacing: 12) { + self.createIcon(style: style) + self.createNamePosition(style: style) + } + .leftAligned() + + self.createPriceChange(style: style) + .rightAligned() + } + .lineLimit(1) + .minimumScaleFactor(0.5) + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + + return AnyView(view) + } + } + + private func createIcon(style: ThemeStyle) -> some View { + let placeholderText = { [weak self] in + if let assetName = self?.assetName { + return Text(assetName.prefix(1)) + .frame(width: 32, height: 32) + .themeColor(foreground: .textTertiary) + .themeColor(background: .layer5) + .borderAndClip(style: .circle, borderColor: .layer7, lineWidth: 1) + .wrappedInAnyView() + } + return AnyView(PlatformView.nilView) + } + let iconType = PlatformIconViewModel.IconType.url(url: URL(string: iconUrl ?? ""), placeholderContent: placeholderText) + return PlatformIconViewModel(type: iconType, + clip: .circle(background: .transparent, spacing: 0), + size: CGSize(width: 32, height: 32)) + .createView(parentStyle: style) + } + + private func createNamePosition(style: ThemeStyle) -> some View { + VStack(alignment: .leading, spacing: 4) { + Text(assetName) + .themeColor(foreground: .textPrimary) + .themeFont(fontSize: .medium) + + HStack { + sideText.createView(parentStyle: style.themeFont(fontSize: .small)) + if let leverage = leverage { + Text(leverage) + .themeColor(foreground: .textSecondary) + .themeFont(fontSize: .small) + } + } + } + } + + private func createPriceChange(style: ThemeStyle) -> some View { + VStack(alignment: .trailing, spacing: 4) { + Text(price ?? "") + .themeColor(foreground: .textPrimary) + .themeFont(fontSize: .medium) + + change?.createView(parentStyle: style.themeFont(fontSize: .small)) + } + } +} + +#if DEBUG +struct dydxSimpleUIMarketView_Previews_Dark: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + ThemeSettings.applyDarkTheme() + ThemeSettings.applyStyles() + return dydxSimpleUIMarketViewModel.previewValue + .createView() + .themeColor(background: .layer0) + .environmentObject(themeSettings) + // .edgesIgnoringSafeArea(.bottom) + .previewLayout(.sizeThatFits) + } +} + +struct dydxSimpleUIMarketView_Previews_Light: PreviewProvider { + @StateObject static var themeSettings = ThemeSettings.shared + + static var previews: some View { + ThemeSettings.applyLightTheme() + ThemeSettings.applyStyles() + return dydxSimpleUIMarketViewModel.previewValue + .createView() + .themeColor(background: .layer0) + .environmentObject(themeSettings) + // .edgesIgnoringSafeArea(.bottom) + .previewLayout(.sizeThatFits) + } +} +#endif diff --git a/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/dydxSimpleUIMarketsView.swift b/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/dydxSimpleUIMarketsView.swift index 8f6b9c15..816c89ab 100644 --- a/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/dydxSimpleUIMarketsView.swift +++ b/dydx/dydxViews/dydxViews/_v4/SimpleUI/Markets/dydxSimpleUIMarketsView.swift @@ -12,6 +12,9 @@ import Utilities public class dydxSimpleUIMarketsViewModel: PlatformViewModel { @Published public var onSettingTapped: (() -> Void)? + @Published public var marketList: dydxSimpleUIMarketListViewModel? + @Published public var marketSearch: dydxSimpleUIMarketSearchViewModel? + @Published public var keyboardUp: Bool = false public init() { } @@ -38,14 +41,31 @@ public class dydxSimpleUIMarketsViewModel: PlatformViewModel { } .padding(.horizontal) - Spacer() + ZStack(alignment: .bottom) { + ScrollView(.vertical, showsIndicators: false) { + LazyVStack(pinnedViews: [.sectionHeaders]) { + if self.keyboardUp == false { + Section { + Spacer() + .frame(height: 320) + } + } + + Section { + self.marketList?.createView(parentStyle: style) + } + } + .keyboardObserving() + } + // .padding(.bottom, 50) // Button height + additional spacing + + self.marketSearch?.createView(parentStyle: style) + } } .frame(maxWidth: .infinity) - .themeColor(background: .layer2) + .themeColor(background: .transparent) - return AnyView( - view - ) + return AnyView(view) } } }