From c224399a37270f574a9f728610c917862c279708 Mon Sep 17 00:00:00 2001 From: mike-dydx <149746839+mike-dydx@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:09:22 -0700 Subject: [PATCH] CLI-628: vault landing screen positions list (#234) * add sections to vault position view * scrolling touchups * keep chart vm in memory * polish * remove unnecessary chart notifications --------- Co-authored-by: Mike --- .../_v4/Vault/dydxVaultViewBuilder.swift | 43 ++++- .../dydxViews.xcodeproj/project.pbxproj | 8 + .../dydxViews/dydxViews/Shared/SideText.swift | 11 ++ .../dydxViews/Shared/SparklineChart.swift | 57 ++++++ .../dydxViews/Shared/TokenText.swift | 2 + .../Landing/dydxVaultChartViewModel.swift | 44 +---- .../Vault/Landing/dydxVaultPositionView.swift | 169 ++++++++++++++++++ .../Vault/Landing/dydxVaultViewModel.swift | 122 ++++++++++--- 8 files changed, 392 insertions(+), 64 deletions(-) create mode 100644 dydx/dydxViews/dydxViews/Shared/SparklineChart.swift create mode 100644 dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultPositionView.swift diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Vault/dydxVaultViewBuilder.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Vault/dydxVaultViewBuilder.swift index 850f9183b..13a16344b 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Vault/dydxVaultViewBuilder.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Vault/dydxVaultViewBuilder.swift @@ -13,12 +13,13 @@ import PlatformParticles import RoutingKit import ParticlesKit import PlatformUI +import Charts public class dydxVaultViewBuilder: NSObject, ObjectBuilderProtocol { public func build() -> T? { let presenter = dydxVaultViewBuilderPresenter() let view = presenter.viewModel?.createView() ?? PlatformViewModel().createView() - return dydxVaultViewController(presenter: presenter, view: view, configuration: .default) as? T + return dydxVaultViewController(presenter: presenter, view: view, configuration: .tabbarItemView) as? T // return HostingViewController(presenter: presenter, view: view) as? T } } @@ -41,5 +42,45 @@ private class dydxVaultViewBuilderPresenter: HostedViewPresenter [ChartDataEntry] { + let selectedValueTime = viewModel?.vaultChart?.selectedValueTime ?? .oneDay + let now = Date().timeIntervalSince1970 + let finalTimeSecondsAway = selectedValueTime == .oneDay ? 3600.0*24.0 : selectedValueTime == .sevenDays ? 3600.0*24.0*7.0 : 3600.0*24.0*30.0 + let numEntries = Int.random(in: 0..<100) + let entries = (0.. [dydxVaultPositionViewModel] { + return [ + dydxVaultPositionViewModel(assetName: "logo_bitcoin", market: "BTC", side: .long, leverage: 10.80, notionalValue: 100000, positionSize: 10000, token: "BTC", tokenUnitPrecision: 6, pnlAmount: 1000, pnlPercentage: 10, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + dydxVaultPositionViewModel(assetName: "logo_ethereum", market: "ETH", side: .short, leverage: 88.88, notionalValue: 50000, positionSize: 10000, token: "ETH", tokenUnitPrecision: -1, pnlAmount: -500, pnlPercentage: -1, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + dydxVaultPositionViewModel(assetName: "logo_bitcoin", market: "BTC", side: .long, leverage: 10.80, notionalValue: 100000, positionSize: 10000, token: "BTC", tokenUnitPrecision: 6, pnlAmount: 1000, pnlPercentage: 10, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + dydxVaultPositionViewModel(assetName: "logo_ethereum", market: "ETH", side: .short, leverage: 88.88, notionalValue: 50000, positionSize: 10000, token: "ETH", tokenUnitPrecision: -1, pnlAmount: -500, pnlPercentage: -1, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + dydxVaultPositionViewModel(assetName: "logo_bitcoin", market: "BTC", side: .long, leverage: 10.80, notionalValue: 100000, positionSize: 10000, token: "BTC", tokenUnitPrecision: 6, pnlAmount: 1000, pnlPercentage: 10, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + dydxVaultPositionViewModel(assetName: "logo_ethereum", market: "ETH", side: .short, leverage: 88.88, notionalValue: 50000, positionSize: 10000, token: "ETH", tokenUnitPrecision: -1, pnlAmount: -500, pnlPercentage: -1, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + dydxVaultPositionViewModel(assetName: "logo_bitcoin", market: "BTC", side: .long, leverage: 10.80, notionalValue: 100000, positionSize: 10000, token: "BTC", tokenUnitPrecision: 6, pnlAmount: 1000, pnlPercentage: 10, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + dydxVaultPositionViewModel(assetName: "logo_ethereum", market: "ETH", side: .short, leverage: 88.88, notionalValue: 50000, positionSize: 10000, token: "ETH", tokenUnitPrecision: -1, pnlAmount: -500, pnlPercentage: -1, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + dydxVaultPositionViewModel(assetName: "logo_bitcoin", market: "BTC", side: .long, leverage: 10.80, notionalValue: 100000, positionSize: 10000, token: "BTC", tokenUnitPrecision: 6, pnlAmount: 1000, pnlPercentage: 10, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + dydxVaultPositionViewModel(assetName: "logo_ethereum", market: "ETH", side: .short, leverage: 88.88, notionalValue: 50000, positionSize: 10000, token: "ETH", tokenUnitPrecision: -1, pnlAmount: -500, pnlPercentage: -1, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + dydxVaultPositionViewModel(assetName: "logo_bitcoin", market: "BTC", side: .long, leverage: 10.80, notionalValue: 100000, positionSize: 10000, token: "BTC", tokenUnitPrecision: 6, pnlAmount: 1000, pnlPercentage: 10, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + dydxVaultPositionViewModel(assetName: "logo_ethereum", market: "ETH", side: .short, leverage: 88.88, notionalValue: 50000, positionSize: 10000, token: "ETH", tokenUnitPrecision: -1, pnlAmount: -500, pnlPercentage: -1, sparklineValues: (0..<10).map { _ in Double.random(in: 0.0...1.0) }), + ] + } } diff --git a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj index c4522891f..8abc78b29 100644 --- a/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj +++ b/dydx/dydxViews/dydxViews.xcodeproj/project.pbxproj @@ -204,6 +204,8 @@ 27DBF3C92C4A05B9009EB2D6 /* dydxTitledNumberField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27DBF3C82C4A05B9009EB2D6 /* dydxTitledNumberField.swift */; }; 27E072D22C1A095C0034B963 /* dydxPortfolioPendingPositionsItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E072D12C1A095C0034B963 /* dydxPortfolioPendingPositionsItemViewModel.swift */; }; 27E0736B2C20D27F0034B963 /* dydxCancelPendingIsolatedOrdersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E0736A2C20D27F0034B963 /* dydxCancelPendingIsolatedOrdersView.swift */; }; + 27EB25832C6D1E5E008C187B /* dydxVaultPositionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EB25822C6D1E5E008C187B /* dydxVaultPositionView.swift */; }; + 27EB25852C6D28BF008C187B /* SparklineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EB25842C6D28BF008C187B /* SparklineChart.swift */; }; 27ED340C2AD47CB100C159F5 /* dydxBannerErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27ED340B2AD47CB100C159F5 /* dydxBannerErrorAlert.swift */; }; 27ED365C2AD735A800C159F5 /* dydxSecurityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27ED365B2AD735A800C159F5 /* dydxSecurityView.swift */; }; 27F624112BBD9FEB00AB6D1A /* dydxPriceInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F624042BBD9FEB00AB6D1A /* dydxPriceInputViewModel.swift */; }; @@ -583,6 +585,8 @@ 27DBF3C82C4A05B9009EB2D6 /* dydxTitledNumberField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTitledNumberField.swift; sourceTree = ""; }; 27E072D12C1A095C0034B963 /* dydxPortfolioPendingPositionsItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxPortfolioPendingPositionsItemViewModel.swift; sourceTree = ""; }; 27E0736A2C20D27F0034B963 /* dydxCancelPendingIsolatedOrdersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxCancelPendingIsolatedOrdersView.swift; sourceTree = ""; }; + 27EB25822C6D1E5E008C187B /* dydxVaultPositionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxVaultPositionView.swift; sourceTree = ""; }; + 27EB25842C6D28BF008C187B /* SparklineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparklineChart.swift; sourceTree = ""; }; 27ED340B2AD47CB100C159F5 /* dydxBannerErrorAlert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxBannerErrorAlert.swift; sourceTree = ""; }; 27ED365B2AD735A800C159F5 /* dydxSecurityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSecurityView.swift; sourceTree = ""; }; 27F624042BBD9FEB00AB6D1A /* dydxPriceInputViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxPriceInputViewModel.swift; sourceTree = ""; }; @@ -938,6 +942,7 @@ 0273A15E2ACCDDB1001B89F5 /* SelectionBar.swift */, 273F50152B7C3F120034792A /* SignedAmountView.swift */, 27D276222C519C98002775F2 /* dydxComponents */, + 27EB25842C6D28BF008C187B /* SparklineChart.swift */, ); path = Shared; sourceTree = ""; @@ -1500,6 +1505,7 @@ 27C6AE0A2C618044005517B5 /* RadioButtons */, 2751D6542C59646700B36F95 /* dydxVaultViewModel.swift */, 2700A3162C5D72BB00880AFA /* dydxVaultChartViewModel.swift */, + 27EB25822C6D1E5E008C187B /* dydxVaultPositionView.swift */, ); path = Landing; sourceTree = ""; @@ -2066,6 +2072,7 @@ 0279DE892BED402C00F9ECF8 /* dydxAdjustMarginPercentageView.swift in Sources */, 026382EB28F0EF0000F766FA /* dydxMarketPriceCandlesResolutionsView.swift in Sources */, 02F99F3E29E4D5750009B3E8 /* dydxTransferSearchItemView.swift in Sources */, + 27EB25832C6D1E5E008C187B /* dydxVaultPositionView.swift in Sources */, 27DBF3C92C4A05B9009EB2D6 /* dydxTitledNumberField.swift in Sources */, 0284201629AD71B600C0E7CC /* Enums.swift in Sources */, 02678FA629666BE600EE346B /* dydxPortfolioOrdersView.swift in Sources */, @@ -2098,6 +2105,7 @@ 277E918B2B27762F005CCBCB /* dydxRewardsLaunchIncentivesView.swift in Sources */, 0268BBF92A8BE08C00D0C59B /* dydxTransferOutView.swift in Sources */, 0279DE452BEBE75100F9ECF8 /* dydxTargetLeverageCtaButtonView.swift in Sources */, + 27EB25852C6D28BF008C187B /* SparklineChart.swift in Sources */, 0208627A28F4D95F00C9D3A0 /* dydxMarketInfoPagingView.swift in Sources */, 027F3EF72AB93ADC00602E5B /* dydxProfileBalancesViewModel.swift in Sources */, 024B44F52983E38D00E35D54 /* dydxTradeStatusLogoView.swift in Sources */, diff --git a/dydx/dydxViews/dydxViews/Shared/SideText.swift b/dydx/dydxViews/dydxViews/Shared/SideText.swift index 0c4c4ef56..a559e7424 100644 --- a/dydx/dydxViews/dydxViews/Shared/SideText.swift +++ b/dydx/dydxViews/dydxViews/Shared/SideText.swift @@ -50,6 +50,17 @@ public class SideTextViewModel: PlatformViewModel, Hashable { return "" } } + + var color: ThemeColor.SemanticColor { + switch self { + case .long, .buy: + return ThemeSettings.positiveColor + case .short, .sell: + return ThemeSettings.negativeColor + case .custom, .none: + return ThemeColor.SemanticColor.textPrimary + } + } public init(positionSide: PositionSide) { switch positionSide { diff --git a/dydx/dydxViews/dydxViews/Shared/SparklineChart.swift b/dydx/dydxViews/dydxViews/Shared/SparklineChart.swift new file mode 100644 index 000000000..f58e9c54b --- /dev/null +++ b/dydx/dydxViews/dydxViews/Shared/SparklineChart.swift @@ -0,0 +1,57 @@ +// +// SparklineChart.swift +// dydxViews +// +// Created by Michael Maguire on 8/14/24. +// + +import Foundation +import SwiftUI +import Charts +import PlatformUI + +struct SparklineView: View { + @State var values: [Double] + + private var lineChart: some View { + let chart = LineChartView() + chart.data = LineChartData() + chart.xAxis.drawGridLinesEnabled = false + chart.leftAxis.enabled = false + chart.rightAxis.enabled = false + chart.xAxis.enabled = false + chart.setViewPortOffsets(left: 0, top: 0, right: 0, bottom: 0) + chart.pinchZoomEnabled = false + chart.doubleTapToZoomEnabled = false + // enables dragging the highlighted value indicator + chart.dragEnabled = false + chart.legend.enabled = false + + let entries = (0..= (entries.first?.y ?? -Double.infinity) + let color = isPositive ? ThemeSettings.positiveColor.uiColor : ThemeSettings.negativeColor.uiColor + + //colors + dataSet.setColor(color) + + //shapes + dataSet.lineWidth = 1.5 + dataSet.lineCapType = .round + dataSet.mode = .linear + dataSet.label = nil + dataSet.drawCirclesEnabled = false + dataSet.drawValuesEnabled = false + + // interactions + dataSet.highlightEnabled = false + dataSet.drawHorizontalHighlightIndicatorEnabled = false + + chart.data = LineChartData(dataSet: dataSet) + return chart.swiftUIView + } + + var body: some View { + lineChart + } +} diff --git a/dydx/dydxViews/dydxViews/Shared/TokenText.swift b/dydx/dydxViews/dydxViews/Shared/TokenText.swift index cc3e71906..402091c4c 100644 --- a/dydx/dydxViews/dydxViews/Shared/TokenText.swift +++ b/dydx/dydxViews/dydxViews/Shared/TokenText.swift @@ -26,6 +26,8 @@ public class TokenTextViewModel: PlatformViewModel, Hashable { return AnyView( Group { Text(self.symbol) + .lineLimit(1) + .minimumScaleFactor(0.5) .padding(.vertical, 1) .padding(.horizontal, 3) } diff --git a/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultChartViewModel.swift b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultChartViewModel.swift index 906ad2fd4..39255fe66 100644 --- a/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultChartViewModel.swift +++ b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultChartViewModel.swift @@ -15,18 +15,12 @@ import dydxChart public class dydxVaultChartViewModel: PlatformViewModel { - @Published var selectedValueType: ValueTypeOption = .pnl - @Published var selectedValueTime: ValueTimeOption = .oneDay { - didSet { - //TODO: remove, just for testing - guard oldValue != selectedValueTime else { return } - setEntries(selectedValueTime: selectedValueTime, selectedValueType: selectedValueType) - } - } + @Published public var selectedValueType: ValueTypeOption = .pnl + @Published public var selectedValueTime: ValueTimeOption = .oneDay fileprivate let valueTypeOptions = ValueTypeOption.allCases fileprivate let valueTimeOptions = ValueTimeOption.allCases - + fileprivate let lineChart = { let lineChart = LineChartView() lineChart.data = LineChartData() @@ -55,23 +49,10 @@ public class dydxVaultChartViewModel: PlatformViewModel { return lineChart }() - // TODO: replace with actual data, delete cancellables - public func setEntries(entries: [ChartDataEntry] = [], selectedValueTime newSelectedValueTime: ValueTimeOption? = nil, selectedValueType newSelectedValueType: ValueTypeOption? = nil) { - if let newSelectedValueType { - selectedValueType = newSelectedValueType - } - if let newSelectedValueTime { - selectedValueTime = newSelectedValueTime - } - //TODO: remove - // this is just for testing - let now = Date().timeIntervalSince1970 - let finalTimeSecondsAway = selectedValueTime == .oneDay ? 3600.0*24.0 : selectedValueTime == .sevenDays ? 3600.0*24.0*7.0 : 3600.0*24.0*30.0 - let numEntries = Int.random(in: 0..<100) - let entries = (0..= (entries.first?.y ?? -Double.infinity) let color = isPositive ? ThemeSettings.positiveColor.uiColor : ThemeSettings.negativeColor.uiColor @@ -102,17 +83,6 @@ public class dydxVaultChartViewModel: PlatformViewModel { lineChart.data = LineChartData(dataSet: dataSet) } - - // TODO: delete and replace with real data - private var cancellables = Set() - init() { - super.init() - Timer.publish(every: 1, triggerNow: true) - .sink { [weak self] _ in - self?.setEntries() - } - .store(in: &cancellables) - } public enum ValueTypeOption: CaseIterable, RadioButtonContentDisplayable { case pnl diff --git a/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultPositionView.swift b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultPositionView.swift new file mode 100644 index 000000000..00573fe14 --- /dev/null +++ b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultPositionView.swift @@ -0,0 +1,169 @@ +// +// dydxVaultPositionView.swift +// dydxUI +// +// Created by Michael Maguire on 8/14/24. +// Copyright © 2024 dYdX Trading Inc. All rights reserved. +// + +import SwiftUI +import PlatformUI +import Utilities +import Charts +import dydxFormatter + +public class dydxVaultPositionViewModel: PlatformViewModel { + static var marketSectionWidth: CGFloat = 130 + static var interSectionPadding: CGFloat = 12 + static var sparklineWidth: CGFloat = 24 + static var pnlSpacing: CGFloat = 3 + + @Published public var assetName: String + @Published public var market: String + @Published public var side: SideTextViewModel.Side + @Published public var leverage: Double + @Published public var notionalValue: Double + @Published public var positionSize: Double + @Published public var tokenUnitPrecision: Int + @Published public var token: String + @Published public var pnlAmount: Double + @Published public var pnlPercentage: Double + @Published public var sparklineValues: [Double] + + fileprivate var sideLeverageAttributedText: AttributedString { + let attributedSideText = AttributedString(text: side.text, urlString: nil) + .themeColor(foreground: side.color) + let leverageText = dydxFormatter.shared.leverage(number: leverage) ?? "--" + let attributedLeverageText = AttributedString(text: "@ " + leverageText, urlString: nil) + .themeColor(foreground: .textTertiary) + return (attributedSideText + attributedLeverageText) + .themeFont(fontType: .base, fontSize: .smaller) + } + + fileprivate var notionalValueText: String { + dydxFormatter.shared.dollar(number: notionalValue) ?? "--" + } + + fileprivate var positionSizeText: String { + dydxFormatter.shared.localFormatted(number: positionSize, digits: tokenUnitPrecision) ?? "--" + } + + fileprivate var pnlColor: ThemeColor.SemanticColor { + pnlAmount >= 0 ? ThemeSettings.positiveColor : ThemeSettings.negativeColor + } + + fileprivate var pnlAmountText: String { + dydxFormatter.shared.dollar(number: pnlAmount) ?? "--" + } + + fileprivate var pnlPercentageText: String { + dydxFormatter.shared.percent(number: pnlPercentage, digits: 2) ?? "--" + } + + public init( + assetName: String, + market: String, + side: SideTextViewModel.Side, + leverage: Double, + notionalValue: Double, + positionSize: Double, + token: String, + tokenUnitPrecision: Int, + pnlAmount: Double, + pnlPercentage: Double, + sparklineValues: [Double]) { + self.assetName = assetName + self.market = market + self.side = side + self.leverage = leverage + self.notionalValue = notionalValue + self.positionSize = positionSize + self.token = token + self.tokenUnitPrecision = tokenUnitPrecision + self.pnlAmount = pnlAmount + self.pnlPercentage = pnlPercentage + self.sparklineValues = sparklineValues + } + + 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 VaultPositionView(viewModel: self) + .wrappedInAnyView() + } + } +} + +private struct VaultPositionView: View { + @ObservedObject var viewModel: dydxVaultPositionViewModel + + var marketSection: some View { + HStack(spacing: 8) { + PlatformIconViewModel(type: .asset(name: viewModel.assetName, bundle: .dydxView), + clip: .circle(background: .transparent, spacing: 0, borderColor: nil), + size: .init(width: 24, height: 24), + templateColor: nil) + .createView() + VStack(alignment: .leading, spacing: 2) { + Text(viewModel.market) + .themeFont(fontType: .base, fontSize: .small) + .themeColor(foreground: .textSecondary) + .lineLimit(1) + .minimumScaleFactor(0.5) + Text(viewModel.sideLeverageAttributedText) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + } + .leftAligned() + } + + var sizeSection: some View { + VStack(alignment: .leading, spacing: 2) { + Text(viewModel.notionalValueText) + .themeFont(fontType: .base, fontSize: .small) + .themeColor(foreground: .textSecondary) + .lineLimit(1) + .minimumScaleFactor(0.5) + HStack(alignment: .top, spacing: 2) { + Text(viewModel.positionSizeText) + .themeFont(fontType: .base, fontSize: .smaller) + .themeColor(foreground: .textTertiary) + .lineLimit(1) + .minimumScaleFactor(0.5) + TokenTextViewModel(symbol: viewModel.token) + .createView(parentStyle: ThemeStyle.defaultStyle.themeFont(fontSize: .smallest)) + } + } + .leftAligned() + } + + var pnlSection: some View { + HStack(alignment: .center, spacing: dydxVaultPositionViewModel.pnlSpacing) { + VStack(alignment: .trailing, spacing: 2) { + Text(viewModel.pnlAmountText) + .themeFont(fontType: .base, fontSize: .small) + .themeColor(foreground: viewModel.pnlColor) + .lineLimit(1) + .minimumScaleFactor(0.5) + Text(viewModel.pnlPercentageText) + .themeFont(fontType: .base, fontSize: .smaller) + .themeColor(foreground: .textTertiary) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + SparklineView(values: viewModel.sparklineValues) + .frame(width: dydxVaultPositionViewModel.sparklineWidth, height: 24) + } + } + + var body: some View { + HStack(spacing: dydxVaultPositionViewModel.interSectionPadding) { + marketSection + .frame(width: dydxVaultPositionViewModel.marketSectionWidth) + sizeSection + pnlSection + } + } +} + diff --git a/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultViewModel.swift b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultViewModel.swift index 843ea69c7..4a2fa90b0 100644 --- a/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultViewModel.swift +++ b/dydx/dydxViews/dydxViews/_v4/Vault/Landing/dydxVaultViewModel.swift @@ -15,37 +15,49 @@ public class dydxVaultViewModel: PlatformViewModel { @Published public var vaultBalance: Double? @Published public var profitDollars: Double? @Published public var profitPercentage: Double? + @Published public var vaultChart: dydxVaultChartViewModel? + @Published public var positions: [dydxVaultPositionViewModel]? @Published public var cancelAction: (() -> Void)? @Published public var learnMoreAction: (() -> Void)? - public init() { } - 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 AnyView(dydxVaultView(viewModel: self)).wrappedInAnyView() + return AnyView(dydxVaultView(viewModel: self)) + .wrappedInAnyView() } } } private struct dydxVaultView: View { @ObservedObject var viewModel: dydxVaultViewModel - + var body: some View { - VStack(spacing: 0) { + VStack { + Spacer().frame(height: 12) titleRow - Spacer().frame(height: 28) - vaultPnlRow - Spacer().frame(height: 16) - div - Spacer().frame(height: 16) - aprTvlRow - Spacer().frame(height: 16) - div - Spacer().frame(height: 16) - chart - Spacer() - + Spacer().frame(height: 20) + ScrollView { + LazyVStack(pinnedViews: [.sectionHeaders]) { + VStack(spacing: 0) { + vaultPnlRow + Spacer().frame(height: 16) + div + Spacer().frame(height: 16) + aprTvlRow + Spacer().frame(height: 16) + div + Spacer().frame(height: 16) + chart + Spacer().frame(height: 16) + div + Spacer().frame(height: 16) + } + Section(header: positionsStickyHeader) { + positionsList + } + } + } } .frame(maxWidth: .infinity) .themeColor(background: .layer2) @@ -60,18 +72,12 @@ private struct dydxVaultView: View { // MARK: - Header var titleRow: some View { HStack(spacing: 16) { - backButton titleImage titleText Spacer() learnMore } - .padding(.horizontal, 12) - } - - var backButton: some View { - ChevronBackButtonModel(onBackButtonTap: viewModel.cancelAction ?? {}) - .createView() + .padding(.horizontal, 16) } var titleImage: some View { @@ -151,7 +157,6 @@ private struct dydxVaultView: View { tvlTitleValue } .leftAligned() - .padding(.horizontal, 16) } var aprTitleValue: some View { @@ -178,8 +183,73 @@ private struct dydxVaultView: View { // MARK: - Section 3 - graph var chart: some View { - dydxVaultChartViewModel() + viewModel.vaultChart? .createView() .frame(height: 174) } + + // MARK: - Section 4 - positions + var openPositionsHeader: some View { + HStack(spacing: 8) { + Text(DataLocalizer.shared?.localize(path: "APP.TRADE.OPEN_POSITIONS", params: nil) ?? "") + .themeColor(foreground: .textSecondary) + .themeFont(fontType: .base, fontSize: .larger) + Text("\(viewModel.positions?.count ?? 0)") + .themeColor(foreground: .textSecondary) + .themeFont(fontType: .base, fontSize: .small) + .padding(.vertical, 2.5) + .padding(.horizontal, 6.5) + .themeColor(background: .layer6) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + .leftAligned() + .padding(.horizontal, 16) + } + + private var positionsStickyHeader: some View { + VStack(spacing: 0) { + openPositionsHeader + Spacer().frame(height: 8) + div + Spacer().frame(height: 16) + positionsColumnsHeader + Spacer().frame(height: 8) + } + .themeColor(background: .layer2) + } + + var positionsColumnsHeader: some View { + HStack(spacing: dydxVaultPositionViewModel.interSectionPadding) { + Group { + Text(DataLocalizer.shared?.localize(path: "APP.GENERAL.MARKET", params: nil) ?? "") + .themeColor(foreground: .textTertiary) + .themeFont(fontType: .base, fontSize: .small) + .leftAligned() + .frame(width: dydxVaultPositionViewModel.marketSectionWidth) + .lineLimit(1) + Text(DataLocalizer.shared?.localize(path: "APP.GENERAL.SIZE", params: nil) ?? "") + .themeColor(foreground: .textTertiary) + .themeFont(fontType: .base, fontSize: .small) + .lineLimit(1) + Spacer() + Text(DataLocalizer.shared?.localize(path: "APP.VAULTS.VAULT_THIRTY_DAY_PNL", params: nil) ?? "") + .themeColor(foreground: .textTertiary) + .themeFont(fontType: .base, fontSize: .small) + .lineLimit(1) + .fixedSize(horizontal: true, vertical: false) + .padding(.trailing, dydxVaultPositionViewModel.pnlSpacing + dydxVaultPositionViewModel.sparklineWidth) + } + } + .padding(.horizontal, 16) + } + + var positionsList: some View { + ForEach(viewModel.positions ?? [], id: \.id) { position in + position.createView() + .centerAligned() + .frame(height: 53) + div + } + .padding(.horizontal, 16) + } }