Skip to content

Commit

Permalink
CLI-638: iOS prediction markets UI (#226)
Browse files Browse the repository at this point in the history
* add footer text when prediction markets is selected filter

* presenting prediction markets notice

* new `present_over_full_screen` presentation style

* speed up animation, improve timing for trade input presentation

* add checkbox

* remove test code

* touchups

* change logic to display prediction markets notice

---------

Co-authored-by: Mike <[email protected]>
  • Loading branch information
mike-dydx and mike-dydx authored Aug 13, 2024
1 parent 3547e49 commit 590db8e
Show file tree
Hide file tree
Showing 17 changed files with 330 additions and 34 deletions.
16 changes: 16 additions & 0 deletions PlatformRouting/PlatformRouting/_iOS/_App/MappedUIKitRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ open class MappedUIKitRouter: MappedRouter {
case .popup:
popup(viewController, animated: animated, completion: completion)

case .presentOverFullScreen:
presentOverFullScreen(viewController, animated: animated, completion: completion)
}
}

Expand Down Expand Up @@ -372,6 +374,20 @@ open class MappedUIKitRouter: MappedRouter {
completion?(nil, false)
}
}

private func presentOverFullScreen(_ viewController: UIViewController, animated: Bool, completion: RoutingCompletionBlock?) {
if let topmost = ViewControllerStack.shared?.topParent() {
let navigationController = UIViewController.navigation(with: viewController)
navigationController.modalPresentationStyle = .overFullScreen
//speeds up the animation by 2x
navigationController.view.layer.speed = 2
topmost.present(navigationController, animated: animated) {
}
completion?(viewController, true)
} else {
completion?(nil, false)
}
}

private func float(_ viewController: UIViewController, animated: Bool, completion: RoutingCompletionBlock?) {
if let topmost = ViewControllerStack.shared?.topParent() {
Expand Down
54 changes: 37 additions & 17 deletions PlatformUI/PlatformUI/DesignSystem/Theme/ThemeViewModifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ private struct StyleKeyModifier: ViewModifier {

public enum MakeSheetStyle {
case fullScreen, fitSize
/// note this style behaves significantly different. It takes content and rounds the corners, adds insets and safe area ignore.
/// only use on views presented with presentOverFullSreen presentation style
/// these sheets are not drag-to-dismiss
case forPresentedOverCurrentScreen
}

public extension View {
Expand All @@ -213,7 +217,8 @@ private struct SheetViewModifier: ViewModifier {
.clipShape(Capsule())
.padding(.top, topPadding)

if sheetStyle == .fullScreen {
switch sheetStyle {
case .fullScreen:
return AnyView(
ZStack(alignment: .top) {
content
Expand All @@ -223,26 +228,41 @@ private struct SheetViewModifier: ViewModifier {
Spacer()
}
}
.environmentObject(themeSettings)
.environmentObject(themeSettings)
)
} else {
case .fitSize:
return AnyView(
VStack(spacing: 0) {
Spacer()
ZStack(alignment: .top) {
content
.cornerRadius(36, corners: [.topLeft, .topRight])
VStack {
dragIndicator
ZStack(alignment: .bottom) {
VStack(spacing: 0) {
Spacer()
ZStack(alignment: .top) {
content
.cornerRadius(36, corners: [.topLeft, .topRight])
VStack {
dragIndicator
}
}
}
.environmentObject(themeSettings)
}
.environmentObject(themeSettings)
)

case .forPresentedOverCurrentScreen:
return ZStack(alignment: .bottom) {
ThemeColor.SemanticColor.layer0.color
.opacity(0.8)
content
.padding(.top, 24)
.padding(.bottom, max((content.safeAreaInsets?.bottom ?? 0), 16))
.padding(.horizontal, 24)
.themeColor(background: .layer3)
.cornerRadius(36, corners: [.topLeft, .topRight])
}
.ignoresSafeArea(edges: [.all])
.wrappedInAnyView()
}
}
}

// MARK: Make any view a button

public extension View {
Expand Down Expand Up @@ -387,7 +407,7 @@ private struct LeftAlignedModifier: ViewModifier {
func body(content: Content) -> some View {
HStack(spacing: 0) {
content
Spacer()
Spacer(minLength: 0)
}
}
}
Expand Down Expand Up @@ -419,9 +439,9 @@ public extension View {

private struct TopAlignedModifier: ViewModifier {
func body(content: Content) -> some View {
VStack {
VStack(spacing: 0) {
content
Spacer()
Spacer(minLength: 0)
}
}
}
Expand All @@ -436,8 +456,8 @@ public extension View {

private struct BottomAlignedModifier: ViewModifier {
func body(content: Content) -> some View {
VStack {
Spacer()
VStack(spacing: 0) {
Spacer(minLength: 0)
content
}
}
Expand Down
4 changes: 4 additions & 0 deletions RoutingKit/RoutingKit/_Router/MappedRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public enum RoutingPresentation: Int {
case drawer
/// center-screen popup which dims the background
case popup
case presentOverFullScreen
}

private struct PathTuple {
Expand Down Expand Up @@ -77,6 +78,9 @@ public class RoutingMap: NSObject, ParsingProtocol {

case "popup":
presentation = .popup

case "present_over_full_screen":
presentation = .presentOverFullScreen

default:
presentation = nil
Expand Down
4 changes: 4 additions & 0 deletions dydx/dydxPresenters/dydxPresenters.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
276908FF2AAFB22F0075B2D6 /* dydxPortfolioTransfersViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276908FE2AAFB22F0075B2D6 /* dydxPortfolioTransfersViewPresenter.swift */; };
277754352C069F8600E3E985 /* OnboardingAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277754282C069F8600E3E985 /* OnboardingAnalytics.swift */; };
277987512BA33F15006DC5CD /* dydxSelectedMarketStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277987502BA33F15006DC5CD /* dydxSelectedMarketStore.swift */; };
277DB22F2C669A6800964F9B /* dydxPredictionMarketsNoticeViewBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277DB22E2C669A6800964F9B /* dydxPredictionMarketsNoticeViewBuilder.swift */; };
277E8FC92B1E576B005CCBCB /* dydxProfileRewardsViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277E8FC82B1E576B005CCBCB /* dydxProfileRewardsViewPresenter.swift */; };
277E90152B1EA0E3005CCBCB /* dydxTradingRewardsViewPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277E90142B1EA0E3005CCBCB /* dydxTradingRewardsViewPresenter.swift */; };
277E90192B1EA3C3005CCBCB /* dydxRewardsSummaryPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277E90182B1EA3C3005CCBCB /* dydxRewardsSummaryPresenter.swift */; };
Expand Down Expand Up @@ -533,6 +534,7 @@
276908FE2AAFB22F0075B2D6 /* dydxPortfolioTransfersViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxPortfolioTransfersViewPresenter.swift; sourceTree = "<group>"; };
277754282C069F8600E3E985 /* OnboardingAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingAnalytics.swift; sourceTree = "<group>"; };
277987502BA33F15006DC5CD /* dydxSelectedMarketStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxSelectedMarketStore.swift; sourceTree = "<group>"; };
277DB22E2C669A6800964F9B /* dydxPredictionMarketsNoticeViewBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxPredictionMarketsNoticeViewBuilder.swift; sourceTree = "<group>"; };
277E8FC82B1E576B005CCBCB /* dydxProfileRewardsViewPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = dydxProfileRewardsViewPresenter.swift; sourceTree = "<group>"; };
277E90142B1EA0E3005CCBCB /* dydxTradingRewardsViewPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxTradingRewardsViewPresenter.swift; sourceTree = "<group>"; };
277E90182B1EA3C3005CCBCB /* dydxRewardsSummaryPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dydxRewardsSummaryPresenter.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1222,6 +1224,7 @@
277987502BA33F15006DC5CD /* dydxSelectedMarketStore.swift */,
0274B34328F113FD005AF69E /* Components */,
02B841B128EF6C6400C4D25B /* dydxMarketInfoViewBuilder.swift */,
277DB22E2C669A6800964F9B /* dydxPredictionMarketsNoticeViewBuilder.swift */,
);
path = MarketInfo;
sourceTree = "<group>";
Expand Down Expand Up @@ -2066,6 +2069,7 @@
647D0F152A9FB1C600DA7815 /* dydxFrontendAlertsProvider.swift in Sources */,
02D1345828ECF30000B46941 /* dydxMarketsSearchViewBuilder.swift in Sources */,
27044F702BBB1CDF004C750D /* dydxTakeProfitStopLossViewPresenter.swift in Sources */,
277DB22F2C669A6800964F9B /* dydxPredictionMarketsNoticeViewBuilder.swift in Sources */,
64529A4C2AE8705E000810E6 /* dydxUpdateWorker.swift in Sources */,
278A4DA42B8FDD9D003898EB /* dydxRatingsWorker.swift in Sources */,
02DDAD55292587C600CC7531 /* QRCodeDisplayBuilder.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,18 @@
"destination":"Share.storyboard",
"presentation":"half"
},
"/trade/:market":{
"/trade/:market": {
"destination":"dydxPresenters.dydxMarketInfoViewBuilder",
"presentation":"push"
},
"/trade/input":{
"destination":"dydxPresenters.dydxTradeInputViewBuilder",
"presentation":"float"
},
"/trade/prediction_markets_notice": {
"destination":"dydxPresenters.dydxPredictionMarketsNoticeViewBuilder",
"presentation":"present_over_full_screen"
},
"/trade/status":{
"destination":"dydxPresenters.dydxTradeStatusViewBuilder",
"presentation":"prompt"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public enum dydxSettingsStoreKey: String, CaseIterable {
case directionColorPreference = "direction_color_preference"
case shouldDisplayInAppNotifications = "should_display_in_app_notifications"
case gasToken = "gas_token"
case hidePredictionMarketsNoticeKey = "hide_prediction_markets_notice"

public var defaultValue: Any? {
switch self {
Expand All @@ -23,6 +24,7 @@ public enum dydxSettingsStoreKey: String, CaseIterable {
case .directionColorPreference: return "green_is_up"
case .shouldDisplayInAppNotifications: return true
case .gasToken: return "USDC"
case .hidePredictionMarketsNoticeKey: return false
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,21 @@ public class dydxMarketInfoViewBuilder: NSObject, ObjectBuilderProtocol {
}

private class dydxMarketInfoViewController: HostingViewController<PlatformView, dydxMarketInfoViewModel> {
private var hidePredictionMarketsNotice: Bool {
SettingsStore.shared?.value(forKey: dydxSettingsStoreKey.hidePredictionMarketsNoticeKey.rawValue) as? Bool ?? false
}

override public func arrive(to request: RoutingRequest?, animated: Bool) -> Bool {
if request?.path == "/trade" || request?.path == "/market", let presenter = presenter as? dydxMarketInfoViewPresenter {
let selectedMarketId = request?.params?["market"] as? String ?? dydxSelectedMarketsStore.shared.lastSelectedMarket
dydxSelectedMarketsStore.shared.lastSelectedMarket = selectedMarketId
presenter.marketId = selectedMarketId
presenter.shouldDisplayFullTradeInputOnAppear = request?.path == "/trade"
if let sectionRaw = request?.params?["currentSection"] as? String {
let section = PortfolioSection(rawValue: sectionRaw) ?? .positions
let preselectedSection = Section.allSections.map(\.key).firstIndex(of: section) ?? 0
presenter.viewModel?.sections.onSelectionChanged?(preselectedSection)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if request?.path == "/trade" {
Router.shared?.navigate(to: RoutingRequest(path: "/trade/input", params: ["full": "true"]), animated: true, completion: nil)
} else {
Router.shared?.navigate(to: RoutingRequest(path: "/trade/input"), animated: true, completion: nil)
}
}
return true
}
return false
Expand All @@ -55,6 +53,7 @@ private protocol dydxMarketInfoViewPresenterProtocol: HostedViewPresenterProtoco

private class dydxMarketInfoViewPresenter: HostedViewPresenter<dydxMarketInfoViewModel>, dydxMarketInfoViewPresenterProtocol {
@Published var marketId: String?
var shouldDisplayFullTradeInputOnAppear: Bool = false

private let pagingPresenter = dydxMarketInfoPagingViewPresenter()
private let statsPresenter = dydxMarketStatsViewPresenter()
Expand All @@ -80,6 +79,10 @@ private class dydxMarketInfoViewPresenter: HostedViewPresenter<dydxMarketInfoVie
.trades: fillsPresenter,
.funding: fundingPresenter
]

fileprivate static var hidePredictionMarketsNotice: Bool {
get { SettingsStore.shared?.value(forKey: dydxSettingsStoreKey.hidePredictionMarketsNoticeKey.rawValue) as? Bool ?? false }
}

override init() {
let viewModel = dydxMarketInfoViewModel()
Expand Down Expand Up @@ -126,9 +129,9 @@ private class dydxMarketInfoViewPresenter: HostedViewPresenter<dydxMarketInfoVie

override func start() {
super.start()

guard let marketId = marketId else { return }

fillsPresenter.filterByMarketId = marketId
fundingPresenter.filterByMarketId = marketId
ordersPresenter.filterByMarketId = marketId
Expand Down Expand Up @@ -160,6 +163,22 @@ private class dydxMarketInfoViewPresenter: HostedViewPresenter<dydxMarketInfoVie
self?.updatePositionSection(position: position, pendingPosition: pendingPosition)
}
.store(in: &subscriptions)

floatTradeInput()
Publishers.CombineLatest(
AbacusStateManager.shared.state.marketMap,
AbacusStateManager.shared.state.assetMap
)
.first()
.sink {[weak self] marketMap, assetMap in
guard let marketId = self?.marketId,
let assetId = marketMap[marketId]?.assetId,
let asset = assetMap[assetId] else { return }
if asset.tags?.contains("Prediction Market") != true && !Self.hidePredictionMarketsNotice {
Router.shared?.navigate(to: RoutingRequest(path: "/trade/prediction_markets_notice"), animated: true, completion: nil)
}
}
.store(in: &subscriptions)
}

override func stop() {
Expand All @@ -174,6 +193,14 @@ private class dydxMarketInfoViewPresenter: HostedViewPresenter<dydxMarketInfoVie
*/
// AbacusStateManager.shared.setMarket(market: nil)
}

private func floatTradeInput() {
if shouldDisplayFullTradeInputOnAppear {
Router.shared?.navigate(to: RoutingRequest(path: "/trade/input", params: ["full": "true", "market": marketId ?? ""]), animated: true, completion: nil)
} else {
Router.shared?.navigate(to: RoutingRequest(path: "/trade/input", params: ["market": marketId ?? ""]), animated: true, completion: nil)
}
}

private func updatePositionSection(position: SubaccountPosition?, pendingPosition: SubaccountPendingPosition?) {
if let position, position.side.current != PositionSide.none {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// dydxPredictionMarketsNoticeViewBuilder.swift
// dydxPresenters
//
// Created by Michael Maguire on 8/9/24.
//

import Utilities
import dydxViews
import PlatformParticles
import RoutingKit
import ParticlesKit
import PlatformUI

public class dydxPredictionMarketsNoticeViewBuilder: NSObject, ObjectBuilderProtocol {
public func build<T>() -> T? {
let presenter = dydxPredictionMarketsNoticeViewPresenter()
let view = presenter.viewModel?.createView() ?? PlatformViewModel().createView()
return dydxPredictionMarketsNoticeViewController(presenter: presenter, view: view, configuration: .default) as? T
}
}

private class dydxPredictionMarketsNoticeViewController: HostingViewController<PlatformView, dydxPredictionMarketsNoticeViewModel> {
override public func arrive(to request: RoutingRequest?, animated: Bool) -> Bool {
if request?.path == "/trade/prediction_markets_notice" {
return true
}
return false
}
}

private protocol dydxPredictionMarketsNoticeViewPresenterProtocol: HostedViewPresenterProtocol {
var viewModel: dydxPredictionMarketsNoticeViewModel? { get }
}

private class dydxPredictionMarketsNoticeViewPresenter: HostedViewPresenter<dydxPredictionMarketsNoticeViewModel>, dydxPredictionMarketsNoticeViewPresenterProtocol {

fileprivate static var hidePredictionMarketsNotice: Bool {
get { SettingsStore.shared?.value(forKey: dydxSettingsStoreKey.hidePredictionMarketsNoticeKey.rawValue) as? Bool ?? false }
set { SettingsStore.shared?.setValue(newValue, forKey: dydxSettingsStoreKey.hidePredictionMarketsNoticeKey.rawValue) }
}

override init() {
super.init()

viewModel = dydxPredictionMarketsNoticeViewModel()
viewModel?.continueAction = {
Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: true, completion: nil)
}

viewModel?.hidePredictionMarketsNotice = Self.hidePredictionMarketsNotice
viewModel?.$hidePredictionMarketsNotice
.sink { Self.hidePredictionMarketsNotice = $0 }
.store(in: &subscriptions)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ private class dydxMarketsViewPresenter: HostedViewPresenter<dydxMarketsViewModel
viewModel?.filter = dydxMarketAssetFilterViewModel(contents: FilterAction.actions.map(\.content),
onSelectionChanged: { [weak self] selectedIdx in
self?.selectedFilterAction = FilterAction.actions[selectedIdx]
if FilterAction.actions[selectedIdx].type == .predictionMarkets {
self?.viewModel?.filterFooterText = DataLocalizer.localize(path: "APP.PREDICTION_MARKET.PREDICTION_MARKETS_SETTLEMENT_DESCRIPTION")
} else {
self?.viewModel?.filterFooterText = nil
}
})
viewModel?.sort = dydxMarketAssetSortViewModel(contents: SortAction.actions.map(\.text)) { [weak self] selectedIdx in
self?.selectedSortAction = SortAction.actions[selectedIdx]
Expand Down
Loading

0 comments on commit 590db8e

Please sign in to comment.