From 51822e04f9caa09f264fb9619f096b72c1ae5829 Mon Sep 17 00:00:00 2001 From: Ermat Date: Tue, 10 Oct 2023 14:17:55 +0600 Subject: [PATCH] Initial (not complete) implementation of CoinOverview module in SwiftUI --- .../project.pbxproj | 18 +++ .../Modules/Chart/ChartUiView.swift | 4 + .../Modules/Chart/ChartView.swift | 19 +++ .../Coin/CoinChart/CoinChartViewModel.swift | 8 +- .../CoinOverview/CoinOverviewModule.swift | 73 ++++++--- .../Coin/CoinOverview/CoinOverviewView.swift | 135 ++++++++++++++++ .../CoinOverviewViewModelNew.swift | 152 ++++++++++++++++++ .../Main/ChartIndicatorsModule.swift | 23 ++- .../SwiftUI/SecondaryCircleButtonStyle.swift | 2 +- 9 files changed, 410 insertions(+), 24 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModelNew.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 52bc5525c9..4a0d21ac3c 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -338,6 +338,7 @@ 11B353D3A4F2305366835086 /* NftActivityHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351EC6F1B4D72D52B4D16 /* NftActivityHeaderView.swift */; }; 11B353DE48A4B088210D927D /* CoinAnalyticsRatingScaleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FDC67CE58FBE44A4107 /* CoinAnalyticsRatingScaleViewController.swift */; }; 11B353E15F4A208D393C7262 /* MarketCategoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3584888F2DB8CCFAA90DF /* MarketCategoryViewController.swift */; }; + 11B353E4793549B6A4F23997 /* CoinOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3506BFA73130CA9A1FF71 /* CoinOverviewView.swift */; }; 11B353E61A5496074178741C /* SendAvailableBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3526E11EC0F9CFCC69D17 /* SendAvailableBalanceViewModel.swift */; }; 11B353E7A2462E19D946E723 /* CellComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355436F62829DBE3C92B4 /* CellComponent.swift */; }; 11B353EAF32244B06E44FAD1 /* PrivateKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A9DB4112F41D7FCAC12 /* PrivateKeysViewModel.swift */; }; @@ -624,6 +625,7 @@ 11B357A9F8949912C12A17D7 /* NftCollectionOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351436E090F4C05243103 /* NftCollectionOverviewViewModel.swift */; }; 11B357ADA154348A3C1A987B /* CoinTreasuriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351E253E310F1738EBE13 /* CoinTreasuriesViewController.swift */; }; 11B357AE8B51E09D0EB60D87 /* NftPriceRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B451378835F7F060012 /* NftPriceRecord.swift */; }; + 11B357BA09F0FA21477F0A59 /* CoinOverviewViewModelNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35363A530051B79BFFFD0 /* CoinOverviewViewModelNew.swift */; }; 11B357BADA228BE93B8451E7 /* AddEvmSyncSourceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350B29037572DDAAF9E16 /* AddEvmSyncSourceViewModel.swift */; }; 11B357BD9D9681D0D79DDEBE /* UITabBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350369A891BEA3A525E5B /* UITabBarItem.swift */; }; 11B357BF378060E7E35F7052 /* AdditionalDataCellNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E67C1B1AB7A13074894 /* AdditionalDataCellNew.swift */; }; @@ -783,6 +785,7 @@ 11B35967B7F22E0C689C5220 /* CoinMarketsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E4058159A4FE60A3F53 /* CoinMarketsService.swift */; }; 11B35968A3A43727ED6FB0B7 /* FavoriteCoinRecordStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C7B8BA65E9AA3BB7AFB /* FavoriteCoinRecordStorage.swift */; }; 11B35968D5BDA7A46C900548 /* AddressInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A382720D6531AE92F72 /* AddressInputView.swift */; }; + 11B3596AE38880C5899769D5 /* CoinOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3506BFA73130CA9A1FF71 /* CoinOverviewView.swift */; }; 11B3596F09D52300F7F0067D /* NftCollectionOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAABF1F6A9EFF769C47 /* NftCollectionOverviewViewController.swift */; }; 11B35970257A865B76C0BBB9 /* NftEventMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352BACB38FE566F6F575B /* NftEventMetadata.swift */; }; 11B35972FDF15D690466B792 /* SendAvailableBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3526E11EC0F9CFCC69D17 /* SendAvailableBalanceViewModel.swift */; }; @@ -1302,12 +1305,14 @@ 11B35F9CC94DB2BC7B43BB59 /* CoinRankViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1E2AE3DC240D5B785E /* CoinRankViewController.swift */; }; 11B35F9E1AF528B31C6F383C /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352E52084020190C21D8C /* InputView.swift */; }; 11B35F9F489F4B358FCCE893 /* MarkdownParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3543968337A40168D3EB0 /* MarkdownParser.swift */; }; + 11B35FA1970606C12E57C2EA /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AFE2C95FF73F75652D8 /* ChartView.swift */; }; 11B35FA3A00690573A482BAC /* CoinRankViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A1E2AE3DC240D5B785E /* CoinRankViewController.swift */; }; 11B35FA6F9EE876BD65E9AD6 /* LaunchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3595BAA550B6BEC8C3F72 /* LaunchScreen.swift */; }; 11B35FA70EB07440E1576A56 /* RowButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BAA4EA85B4A3A173498 /* RowButtonStyle.swift */; }; 11B35FAB3263E489CB9017FC /* AddTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356D5A5F32E88FEC7629D /* AddTokenViewController.swift */; }; 11B35FB1B7B34756830942DC /* LaunchErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A4096D259C9B1540D10 /* LaunchErrorViewController.swift */; }; 11B35FB28152F8881369DD9D /* AdapterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A4E49ED2D2BF8E60863 /* AdapterManager.swift */; }; + 11B35FB362526C723329C9ED /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35AFE2C95FF73F75652D8 /* ChartView.swift */; }; 11B35FB3A17F76325C98C2AB /* UnlinkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351A464627DCBABD1AC17 /* UnlinkService.swift */; }; 11B35FB4B6E5E6B442ADE3B2 /* BinanceCexProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B355E9CE0702287077F975 /* BinanceCexProvider.swift */; }; 11B35FB74A0FAB9385945628 /* CoinReportsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35469D625FF263504536F /* CoinReportsModule.swift */; }; @@ -1319,6 +1324,7 @@ 11B35FC6DE83EE46FB361756 /* CexWithdrawModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35507A989EA73EE5E8EA8 /* CexWithdrawModule.swift */; }; 11B35FD18C255E2C6D75F38A /* RestoreMnemonicHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CB288AF5A54B99A51E4 /* RestoreMnemonicHintView.swift */; }; 11B35FD73BCF3DD557FD9783 /* RecipientAddressInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35DBDADDA8D4F9D88C7AA /* RecipientAddressInputCell.swift */; }; + 11B35FDF03CD52FEC5B1745A /* CoinOverviewViewModelNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35363A530051B79BFFFD0 /* CoinOverviewViewModelNew.swift */; }; 11B35FE0809AC8A716C41427 /* PrimaryButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35968D12AAAC828AFE955 /* PrimaryButtonStyle.swift */; }; 11B35FE7DA00590FF95854FF /* WatchPublicKeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350D2A21FC1BE1F457B41 /* WatchPublicKeyViewModel.swift */; }; 11B35FE8D60BFF31C3104484 /* SwitchAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350BC3E707879846AC0AA /* SwitchAccountViewModel.swift */; }; @@ -2773,6 +2779,7 @@ 11B3505AD2C1640DEAD8CFFC /* MarketTopViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketTopViewController.swift; sourceTree = ""; }; 11B350669B3E9E6155F33F23 /* BaseCurrencySettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseCurrencySettingsViewModel.swift; sourceTree = ""; }; 11B3506758F70E9014947BB3 /* CexWithdrawConfirmViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexWithdrawConfirmViewModel.swift; sourceTree = ""; }; + 11B3506BFA73130CA9A1FF71 /* CoinOverviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinOverviewView.swift; sourceTree = ""; }; 11B3506CB3D780A00F4BBBBE /* AccountRecord_v_0_20.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecord_v_0_20.swift; sourceTree = ""; }; 11B35071F0BD63CCE6417ADC /* CexAmountInputViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexAmountInputViewModel.swift; sourceTree = ""; }; 11B3507B0AFFDF51A528A6EE /* RestoreSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSettingsView.swift; sourceTree = ""; }; @@ -2920,6 +2927,7 @@ 11B3534997B5CD413DBDB7C7 /* CoinProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinProvider.swift; sourceTree = ""; }; 11B3534E81EFE21D1F84C130 /* WatchViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchViewModel.swift; sourceTree = ""; }; 11B3535FC407BA20765EBCF4 /* KeyboardObservingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardObservingViewController.swift; sourceTree = ""; }; + 11B35363A530051B79BFFFD0 /* CoinOverviewViewModelNew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinOverviewViewModelNew.swift; sourceTree = ""; }; 11B353684493AFDF3711DF2B /* TokenQuery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenQuery.swift; sourceTree = ""; }; 11B35368FF9DD8600557BF07 /* TextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextCell.swift; sourceTree = ""; }; 11B3536CE69BFC7513A9DFDF /* GuidesService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GuidesService.swift; sourceTree = ""; }; @@ -3241,6 +3249,7 @@ 11B35ADF518A2F98FF673B4B /* CoinAuditsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinAuditsViewModel.swift; sourceTree = ""; }; 11B35ADF9BC4D149F86F23E4 /* MarketFilteredListService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketFilteredListService.swift; sourceTree = ""; }; 11B35AE5785634316A1A5DA8 /* WalletBlockchainElementService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletBlockchainElementService.swift; sourceTree = ""; }; + 11B35AFE2C95FF73F75652D8 /* ChartView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartView.swift; sourceTree = ""; }; 11B35B0A0EC524FBC663BEA5 /* CexDepositViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexDepositViewItemFactory.swift; sourceTree = ""; }; 11B35B106BD8E4DBD67B7700 /* BaseTransactionsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTransactionsService.swift; sourceTree = ""; }; 11B35B109B4F60753BEC5078 /* ReceiveAddressService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAddressService.swift; sourceTree = ""; }; @@ -7560,6 +7569,7 @@ ABC9A76ACF7C7D6D7D3FA323 /* Components */, ABC9A1CF38A26663C93F47B4 /* MarketCards */, 11B35DE812F995B07C8F0B01 /* ChartUiView.swift */, + 11B35AFE2C95FF73F75652D8 /* ChartView.swift */, ); path = Chart; sourceTree = ""; @@ -7817,6 +7827,8 @@ 2FA5D4E16E60866549E0CD48 /* CoinOverviewViewModel.swift */, 2FA5DA1F5A41E633A244DAD1 /* CoinOverviewViewController.swift */, 58AAA0B8ECE5854FAB9362AC /* CoinOverviewViewItemFactory.swift */, + 11B3506BFA73130CA9A1FF71 /* CoinOverviewView.swift */, + 11B35363A530051B79BFFFD0 /* CoinOverviewViewModelNew.swift */, ); path = CoinOverview; sourceTree = ""; @@ -9474,6 +9486,9 @@ ABC9A67C2D782AD0DFDF0C3C /* RestoreFileConfigurationViewController.swift in Sources */, ABC9ACEB81BCB00435B35F64 /* RestoreFileHelper.swift in Sources */, 11B35538EF749777CF7B2E8B /* ChartUiView.swift in Sources */, + 11B35FA1970606C12E57C2EA /* ChartView.swift in Sources */, + 11B3596AE38880C5899769D5 /* CoinOverviewView.swift in Sources */, + 11B357BA09F0FA21477F0A59 /* CoinOverviewViewModelNew.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10803,6 +10818,9 @@ ABC9A0C5DE01B3C50D4C7FF2 /* RestoreFileConfigurationViewController.swift in Sources */, ABC9A3231731F39ECA5B90ED /* RestoreFileHelper.swift in Sources */, 11B356DF455592656B742485 /* ChartUiView.swift in Sources */, + 11B35FB362526C723329C9ED /* ChartView.swift in Sources */, + 11B353E4793549B6A4F23997 /* CoinOverviewView.swift in Sources */, + 11B35FDF03CD52FEC5B1745A /* CoinOverviewViewModelNew.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift index 01ac58e93f..f981478619 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift @@ -219,6 +219,10 @@ class ChartUiView: UIView { fatalError("init(coder:) has not been implemented") } + override var intrinsicContentSize: CGSize { + CGSize(width: UIView.noIntrinsicMetric, height: totalHeight) + } + var totalHeight: CGFloat { .heightDoubleLineCell + configuration.mainHeight diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartView.swift new file mode 100644 index 0000000000..908ead06a3 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartView.swift @@ -0,0 +1,19 @@ +import Chart +import SwiftUI +import UIKit + +struct ChartView: UIViewRepresentable { + typealias UIViewType = UIView + + let viewModel: IChartViewModel & IChartViewTouchDelegate + let configuration: ChartConfiguration + + func makeUIView(context _: Context) -> UIView { + let chartView = ChartUiView(viewModel: viewModel, configuration: configuration) + chartView.setContentHuggingPriority(.required, for: .vertical) + chartView.onLoad() + return chartView + } + + func updateUIView(_: UIView, context _: Context) {} +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift index bf888fb398..5692f14032 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/CoinChartViewModel.swift @@ -6,8 +6,9 @@ import MarketKit import Chart import CurrencyKit import HUD +import Combine -class CoinChartViewModel { +class CoinChartViewModel: ObservableObject { private let service: CoinChartService private let factory: CoinChartFactory private let disposeBag = DisposeBag() @@ -24,6 +25,8 @@ class CoinChartViewModel { private let indicatorsShownRelay = BehaviorRelay(value: true) private let openSettingsRelay = PublishRelay<()>() + @Published private(set) var indicatorsShown: Bool + var intervals: [String] { service.validIntervals.map { $0.title } + ["chart.time_duration.all".localized] } @@ -32,6 +35,8 @@ class CoinChartViewModel { self.service = service self.factory = factory + indicatorsShown = service.indicatorsShown + subscribe(scheduler, disposeBag, service.intervalsUpdatedObservable) { [weak self] in self?.syncIntervalsUpdate() } subscribe(scheduler, disposeBag, service.periodTypeObservable) { [weak self] in self?.sync(periodType: $0) } subscribe(scheduler, disposeBag, service.stateObservable) { [weak self] in self?.sync(state: $0) } @@ -48,6 +53,7 @@ class CoinChartViewModel { private func updateIndicatorsShown() { indicatorsShownRelay.accept(service.indicatorsShown) + indicatorsShown = service.indicatorsShown } private func index(periodType: HsPeriodType) -> Int { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewModule.swift index 41e7b62c8e..c67c3eacf5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewModule.swift @@ -1,30 +1,62 @@ -import MarketKit -import LanguageKit import Chart +import LanguageKit +import MarketKit +import SwiftUI struct CoinOverviewModule { + static func view(coinUid: String) -> some View { + let repository = ChartIndicatorsRepository( + localStorage: App.shared.localStorage, + subscriptionManager: App.shared.subscriptionManager + ) + let chartService = CoinChartService( + marketKit: App.shared.marketKit, + currencyKit: App.shared.currencyKit, + localStorage: App.shared.localStorage, + indicatorRepository: repository, + coinUid: coinUid + ) + let chartFactory = CoinChartFactory(currentLocale: LanguageManager.shared.currentLocale) + let chartViewModel = CoinChartViewModel(service: chartService, factory: chartFactory) + + let viewModel = CoinOverviewViewModelNew( + coinUid: coinUid, + marketKit: App.shared.marketKit, + currencyKit: App.shared.currencyKit, + languageManager: LanguageManager.shared, + accountManager: App.shared.accountManager, + walletManager: App.shared.walletManager + ) + + return CoinOverviewView( + viewModel: viewModel, + chartViewModel: chartViewModel, + chartIndicatorRepository: repository, + chartPointFetcher: chartService + ) + } static func viewController(coinUid: String) -> CoinOverviewViewController { let service = CoinOverviewService( - coinUid: coinUid, - marketKit: App.shared.marketKit, - currencyKit: App.shared.currencyKit, - languageManager: LanguageManager.shared, - accountManager: App.shared.accountManager, - walletManager: App.shared.walletManager + coinUid: coinUid, + marketKit: App.shared.marketKit, + currencyKit: App.shared.currencyKit, + languageManager: LanguageManager.shared, + accountManager: App.shared.accountManager, + walletManager: App.shared.walletManager ) let repository = ChartIndicatorsRepository( - localStorage: App.shared.localStorage, - subscriptionManager: App.shared.subscriptionManager + localStorage: App.shared.localStorage, + subscriptionManager: App.shared.subscriptionManager ) let chartService = CoinChartService( - marketKit: App.shared.marketKit, - currencyKit: App.shared.currencyKit, - localStorage: App.shared.localStorage, - indicatorRepository: repository, - coinUid: coinUid + marketKit: App.shared.marketKit, + currencyKit: App.shared.currencyKit, + localStorage: App.shared.localStorage, + indicatorRepository: repository, + coinUid: coinUid ) let router = ChartIndicatorRouter(repository: repository, fetcher: chartService) @@ -34,12 +66,11 @@ struct CoinOverviewModule { let chartViewModel = CoinChartViewModel(service: chartService, factory: chartFactory) return CoinOverviewViewController( - viewModel: viewModel, - chartViewModel: chartViewModel, - chartRouter: router, - markdownParser: CoinPageMarkdownParser(), - urlManager: UrlManager(inApp: true) + viewModel: viewModel, + chartViewModel: chartViewModel, + chartRouter: router, + markdownParser: CoinPageMarkdownParser(), + urlManager: UrlManager(inApp: true) ) } - } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift new file mode 100644 index 0000000000..661b157722 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewView.swift @@ -0,0 +1,135 @@ +import CurrencyKit +import SDWebImageSwiftUI +import SwiftUI + +struct CoinOverviewView: View { + @ObservedObject var viewModel: CoinOverviewViewModelNew + @ObservedObject var chartViewModel: CoinChartViewModel + let chartIndicatorRepository: IChartIndicatorsRepository + let chartPointFetcher: IChartPointFetcher + + @State private var chartIndicatorsShown = false + + var body: some View { + ThemeView { + ZStack { + switch viewModel.state { + case .loading: + ProgressView() + case let .failed(error): + Text(error.localizedDescription) + case let .completed(item): + let info = item.info + let coin = item.info.fullCoin.coin + let coinCode = coin.code + let rank = info.marketCapRank.map { "#\($0)" } + + ScrollView { + VStack(spacing: 0) { + HStack(spacing: .margin16) { + WebImage(url: URL(string: coin.imageUrl)) + .placeholder(Image("placeholder_circle_32")) + .resizable() + .scaledToFit() + .frame(width: .iconSize32, height: .iconSize32) + + Text(coin.name).themeBody() + + if let rank { + Text(rank).themeSubhead1(alignment: .trailing) + } + } + .padding(.horizontal, .margin16) + .padding(.vertical, .margin12) + + ChartView(viewModel: chartViewModel, configuration: .coinChart) + .frame(maxWidth: .infinity) + .onAppear { + chartViewModel.start() + } + + VStack { + ListSection { + ListRow { + Text("coin_overview.indicators".localized).themeSubhead2() + + Button(action: { + chartViewModel.onToggleIndicators() + }) { + Text(chartViewModel.indicatorsShown ? "coin_overview.indicators.hide".localized : "coin_overview.indicators.show".localized) + .animation(.none) + } + .buttonStyle(SecondaryButtonStyle(style: .default)) + + Button(action: { + chartIndicatorsShown = true + }) { + Image("setting_20").renderingMode(.template) + } + .buttonStyle(SecondaryCircleButtonStyle(style: .default)) + } + } + + let infoItems = [ + format(value: info.marketCap, currency: viewModel.currency).map { + (title: "coin_overview.market_cap".localized, badge: rank, text: $0) + }, + format(value: info.totalSupply, coinCode: coinCode).map { + (title: "coin_overview.total_supply".localized, badge: nil, text: $0) + }, + format(value: info.circulatingSupply, coinCode: coinCode).map { + (title: "coin_overview.circulating_supply".localized, badge: nil, text: $0) + }, + format(value: info.volume24h, currency: viewModel.currency).map { + (title: "coin_overview.trading_volume".localized, badge: nil, text: $0) + }, + format(value: info.dilutedMarketCap, currency: viewModel.currency).map { + (title: "coin_overview.diluted_market_cap".localized, badge: nil, text: $0) + }, + info.genesisDate.map { + (title: "coin_overview.genesis_date".localized, badge: nil, text: DateHelper.instance.formatFullDateOnly(from: $0)) + }, + ].compactMap { $0 } + + if !infoItems.isEmpty { + ListSection { + ForEach(infoItems, id: \.title) { infoItem in + ListRow { + Text(infoItem.title).themeSubhead2() + Text(infoItem.text).themeSubhead1(color: .themeLeah, alignment: .trailing) + } + } + } + } + } + .padding(EdgeInsets(top: 0, leading: .margin16, bottom: 0, trailing: .margin16)) + } + } + } + } + } + .onAppear { + viewModel.sync() + } + .sheet(isPresented: $chartIndicatorsShown) { + ChartIndicatorsModule.view(repository: chartIndicatorRepository, fetcher: chartPointFetcher) + .ignoresSafeArea() + } + } + + private func format(value: Decimal?, coinCode: String) -> String? { + guard let value = value, !value.isZero else { + return nil + } + + return ValueFormatter.instance.formatShort(value: value, decimalCount: 0, symbol: coinCode) + } + + private func format(value: Decimal?, currency: Currency) -> String? { + guard let value = value, !value.isZero else { + return nil + } + + return ValueFormatter.instance.formatShort(currency: currency, value: value) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModelNew.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModelNew.swift new file mode 100644 index 0000000000..fafc1a6fdb --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinOverview/CoinOverviewViewModelNew.swift @@ -0,0 +1,152 @@ +import Combine +import CurrencyKit +import Foundation +import HsExtensions +import LanguageKit +import MarketKit + +class CoinOverviewViewModelNew: ObservableObject { + private var tasks = Set() + + private let coinUid: String + private let marketKit: MarketKit.Kit + private let currencyKit: CurrencyKit.Kit + private let languageManager: LanguageManager + private let accountManager: AccountManager + private let walletManager: WalletManager + private let viewItemFactory = CoinOverviewViewItemFactory() + + let currency: Currency + + @Published private(set) var state: DataStatus = .loading + + init(coinUid: String, marketKit: MarketKit.Kit, currencyKit: CurrencyKit.Kit, languageManager: LanguageManager, accountManager: AccountManager, walletManager: WalletManager) { + self.coinUid = coinUid + self.marketKit = marketKit + self.currencyKit = currencyKit + self.languageManager = languageManager + self.accountManager = accountManager + self.walletManager = walletManager + + currency = currencyKit.baseCurrency + } + + private func handleSuccess(info: MarketInfoOverview) { + let account = accountManager.activeAccount + + let tokens = info.fullCoin.tokens + .filter { + switch $0.type { + case let .unsupported(_, reference): return reference != nil + default: return true + } + } + + let walletTokens = walletManager.activeWallets.map { + $0.token + } + + let tokenItems = tokens + .sorted { lhsToken, rhsToken in + let lhsTypeOrder = lhsToken.type.order + let rhsTypeOrder = rhsToken.type.order + + guard lhsTypeOrder == rhsTypeOrder else { + return lhsTypeOrder < rhsTypeOrder + } + + return lhsToken.blockchainType.order < rhsToken.blockchainType.order + } + .map { token in + let state: TokenItemState + + if let account = account, !account.watchAccount, account.type.supports(token: token) { + if walletTokens.contains(token) { + state = .alreadyAdded + } else { + state = .canBeAdded + } + } else { + state = .cannotBeAdded + } + + return TokenItem( + token: token, + state: state + ) + } + + DispatchQueue.main.async { + self.state = .completed(Item(info: info, tokens: tokenItems, guideUrl: self.guideUrl)) + } + } + + private func handleFailure(error: Error) { + DispatchQueue.main.async { + self.state = .failed(error) + } + } + + private var guideUrl: URL? { + guard let guideFileUrl = guideFileUrl else { + return nil + } + + return URL(string: guideFileUrl, relativeTo: AppConfig.guidesIndexUrl) + } + + private var guideFileUrl: String? { + switch coinUid { + case "bitcoin": return "guides/token_guides/en/bitcoin.md" + case "ethereum": return "guides/token_guides/en/ethereum.md" + case "bitcoin-cash": return "guides/token_guides/en/bitcoin-cash.md" + case "zcash": return "guides/token_guides/en/zcash.md" + case "uniswap": return "guides/token_guides/en/uniswap.md" + case "curve-dao-token": return "guides/token_guides/en/curve-finance.md" + case "balancer": return "guides/token_guides/en/balancer-dex.md" + case "synthetix-network-token": return "guides/token_guides/en/synthetix.md" + case "tether": return "guides/token_guides/en/tether.md" + case "maker": return "guides/token_guides/en/makerdao.md" + case "dai": return "guides/token_guides/en/makerdao.md" + case "aave": return "guides/token_guides/en/aave.md" + case "compound": return "guides/token_guides/en/compound.md" + default: return nil + } + } +} + +extension CoinOverviewViewModelNew { + func sync() { + tasks = Set() + + state = .loading + + Task { [weak self, marketKit, coinUid, currencyKit, languageManager] in + do { + let info = try await marketKit.marketInfoOverview(coinUid: coinUid, currencyCode: currencyKit.baseCurrency.code, languageCode: languageManager.currentLanguage) + self?.handleSuccess(info: info) + } catch { + self?.handleFailure(error: error) + } + }.store(in: &tasks) + } +} + +extension CoinOverviewViewModelNew { + struct Item { + let info: MarketInfoOverview + let tokens: [TokenItem] + let guideUrl: URL? + } + + struct TokenItem { + let token: Token + let state: TokenItemState + } + + enum TokenItemState { + case canBeAdded + case alreadyAdded + case cannotBeAdded + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsModule.swift index 411bfd1b1e..8ececd3e13 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsModule.swift @@ -1,6 +1,7 @@ import Foundation -import UIKit +import SwiftUI import ThemeKit +import UIKit class ChartIndicatorRouter { private let repository: IChartIndicatorsRepository @@ -17,5 +18,25 @@ class ChartIndicatorRouter { return ThemeNavigationController(rootViewController: ChartIndicatorsViewController(viewModel: viewModel)) } +} + +enum ChartIndicatorsModule { + static func view(repository: IChartIndicatorsRepository, fetcher: IChartPointFetcher) -> some View { + let service = ChartIndicatorsService(repository: repository, chartPointFetcher: fetcher, subscriptionManager: App.shared.subscriptionManager) + let viewModel = ChartIndicatorsViewModel(service: service) + + return ChartIndicatorsView(viewModel: viewModel) + } +} + +struct ChartIndicatorsView: UIViewControllerRepresentable { + typealias UIViewControllerType = UIViewController + + let viewModel: ChartIndicatorsViewModel + + func makeUIViewController(context _: Context) -> UIViewController { + ThemeNavigationController(rootViewController: ChartIndicatorsViewController(viewModel: viewModel)) + } + func updateUIViewController(_: UIViewController, context _: Context) {} } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift index 8442269d98..0960a67cc0 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/SecondaryCircleButtonStyle.swift @@ -3,7 +3,7 @@ import SwiftUI struct SecondaryCircleButtonStyle: ButtonStyle { let style: Style - @Environment(\.isEnabled) var isEnabled + @Environment(\.isEnabled) private var isEnabled func makeBody(configuration: Configuration) -> some View { configuration.label