From 49ab6496ac28801dc53b856463e0003d0ce77539 Mon Sep 17 00:00:00 2001 From: Ermat Date: Thu, 19 Oct 2023 16:42:04 +0600 Subject: [PATCH] Implement Watchlist widget --- .../project.pbxproj | 28 +++++ .../UnstoppableWallet/Core/App.swift | 5 +- .../Modules/Favorites/FavoritesManager.swift | 22 +++- .../Widget/AppWidgetBundle.swift | 1 + .../Widget/AppWidgetConstants.swift | 7 ++ .../rate_48.imageset/Contents.json | 22 ++++ .../rate_48.imageset/rate@2x.png | Bin 0 -> 2221 bytes .../rate_48.imageset/rate@3x.png | Bin 0 -> 3153 bytes .../CoinPriceList/CoinPriceListEntry.swift | 3 +- .../CoinPriceList/CoinPriceListProvider.swift | 39 ++++-- .../CoinPriceList/CoinPriceListView.swift | 118 +++++++++++------- .../Widget/Misc/ApiProvider.swift | 8 +- .../Widget/Misc/CoinPriceListMode.swift | 18 +++ .../Widget/Misc/Extensions.swift | 29 +++++ .../SingleCoinPriceEntry.swift | 6 +- .../SingleCoinPriceProvider.swift | 21 ++-- .../SingleCoinPrice/SingleCoinPriceView.swift | 4 +- .../Widget/SingleCoinPriceWidget.swift | 4 +- UnstoppableWallet/Widget/TopCoinsWidget.swift | 6 +- .../Widget/WatchlistWidget.swift | 30 +++++ 20 files changed, 279 insertions(+), 92 deletions(-) create mode 100644 UnstoppableWallet/Widget/AppWidgetConstants.swift create mode 100644 UnstoppableWallet/Widget/Assets.xcassets/rate_48.imageset/Contents.json create mode 100644 UnstoppableWallet/Widget/Assets.xcassets/rate_48.imageset/rate@2x.png create mode 100644 UnstoppableWallet/Widget/Assets.xcassets/rate_48.imageset/rate@3x.png create mode 100644 UnstoppableWallet/Widget/Misc/CoinPriceListMode.swift create mode 100644 UnstoppableWallet/Widget/Misc/Extensions.swift create mode 100644 UnstoppableWallet/Widget/WatchlistWidget.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index bdfe5921c4..25ea5c80c3 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -239,6 +239,7 @@ 11B352840E06275F96EFFCDB /* BaseCurrencySettingsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B57AFCEAA3AA071F07F /* BaseCurrencySettingsModule.swift */; }; 11B352841C5901ABCC0096C2 /* BlockchainSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3573A58F426A72669A948 /* BlockchainSettingsViewModel.swift */; }; 11B35286305AB7DA6114F1D0 /* SwitchAccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359D884F1698E70F2536E /* SwitchAccountService.swift */; }; + 11B35287E46AFFBC47162F67 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3558D624AF040E9D102DF /* Extensions.swift */; }; 11B35289269C634BC9219362 /* RecoveryPhraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3558ACECAA1C886FA82C0 /* RecoveryPhraseViewController.swift */; }; 11B3528B1101D2E02ECB4631 /* ListSectionInfoHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B23F86488FDB41CC862 /* ListSectionInfoHeader.swift */; }; 11B3528DFBD66C380A185CB7 /* TransactionsCoinSelectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B4D1E2433F5439D9F9A /* TransactionsCoinSelectViewModel.swift */; }; @@ -374,6 +375,7 @@ 11B35416D17D3FB0991A12FD /* ReceiveAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B354F94192C3C8D9011983 /* ReceiveAddressViewController.swift */; }; 11B3541BC8A0EF1066EBA464 /* NonSpamPoolProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359FE5BB60FB12BB24F3E /* NonSpamPoolProvider.swift */; }; 11B3541D3F7AFB8C651BEE2B /* Guide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353B4C04282FDBB1B6563 /* Guide.swift */; }; + 11B3541E8CB5F0F743E9CDF3 /* AppWidgetConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E298D53B8A2C2684119 /* AppWidgetConstants.swift */; }; 11B3541ED37746BAFF1832BA /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352648C452D611F1EDF61 /* Image.swift */; }; 11B3541F6B5316F3B373D1EA /* ValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35EFB45ECC2D403CA6C89 /* ValueFormatter.swift */; }; 11B35421DB6DADDF59764A46 /* SimpleActivateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A3B86D99FBB036C74C7 /* SimpleActivateView.swift */; }; @@ -450,6 +452,7 @@ 11B355028142F82D805752AF /* NftDoubleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35FEC3027F45085959FBB /* NftDoubleCell.swift */; }; 11B35503B3E84FEFCDF1AFED /* ThemeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E7E7A5DBB09A2A5197D /* ThemeView.swift */; }; 11B3550424326606B055D7E5 /* AboutModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B353E80D544DAF20B12B56 /* AboutModule.swift */; }; + 11B3550548CB49D32EAC1DF5 /* WatchlistWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B0879F715C0777919AA /* WatchlistWidget.swift */; }; 11B3550A6826CF513B1A77F0 /* TabHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357D7156B86181DD0C6D4 /* TabHeaderView.swift */; }; 11B3550B0E0438427CBE72A3 /* NftCollectionOverviewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35507299A9DA6CF3C626A /* NftCollectionOverviewModule.swift */; }; 11B3550C46C5811A7540A934 /* ReceiveAddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B109B4F60753BEC5078 /* ReceiveAddressService.swift */; }; @@ -603,6 +606,7 @@ 11B356F5D16C4E630CE980E8 /* CoinMarketsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B7D66631DD5D91D0773 /* CoinMarketsModule.swift */; }; 11B356FB8054678ED5FB5E45 /* FormCautionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3568F6FAF721301DEC188 /* FormCautionView.swift */; }; 11B356FF5E5CC9AA34B89C86 /* HeaderAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352C35227943125FF2008 /* HeaderAmountView.swift */; }; + 11B35700253CCD66C4CCE354 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3558D624AF040E9D102DF /* Extensions.swift */; }; 11B3570D6396DD2F73973E73 /* EvmAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A296048CDD27A26FE9E /* EvmAccountManager.swift */; }; 11B3570F40DFF864DC9436F6 /* RestoreSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3507B0AFFDF51A528A6EE /* RestoreSettingsView.swift */; }; 11B357118379A537844A83D0 /* EvmSyncSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3502637A858E6DDF9471B /* EvmSyncSource.swift */; }; @@ -842,6 +846,7 @@ 11B359A3D67610D8E2E07A0B /* TextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35368FF9DD8600557BF07 /* TextCell.swift */; }; 11B359A4F6C0F8A705FDE18E /* Pool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BDFE0ED8F75095F1441 /* Pool.swift */; }; 11B359A56AAFB4C4F6EF733C /* SharedLocalStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B2E73D0FA19CBC3B674 /* SharedLocalStorage.swift */; }; + 11B359A79743E965870AE8B6 /* AppWidgetConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E298D53B8A2C2684119 /* AppWidgetConstants.swift */; }; 11B359AA65D8E9BE25175DAF /* TermsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B351DAF31FBE0834EBC066 /* TermsService.swift */; }; 11B359AA9A3F1FD68323C64E /* BlockchainSettingRecord_v_0_24.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3526A40F07F6C8E77BEF9 /* BlockchainSettingRecord_v_0_24.swift */; }; 11B359B09D7E49066368CFE0 /* WalletViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3570624266AA63F869105 /* WalletViewItemFactory.swift */; }; @@ -849,6 +854,7 @@ 11B359B7C572FDCA7CD68320 /* FaqService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352B4E116BEC01B972A39 /* FaqService.swift */; }; 11B359BA446B27E6D369B35E /* RestoreModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350BD0CE4F979CA88EFF0 /* RestoreModule.swift */; }; 11B359BD68E234293DCF33CC /* AppStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35496770FA251785E5581 /* AppStatusViewModel.swift */; }; + 11B359BF322A7B912C778348 /* WatchlistWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B0879F715C0777919AA /* WatchlistWidget.swift */; }; 11B359C05619611CBCFC89AC /* EvmBlockchainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B356D15E318829D9C7F5F1 /* EvmBlockchainManager.swift */; }; 11B359C198AA7A141522E5E9 /* EvmAccountManagerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F980B34E005B9F02B8F /* EvmAccountManagerFactory.swift */; }; 11B359C2651DA1F00A3C613C /* CoinRankViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B359575A4E090B236E84C7 /* CoinRankViewModel.swift */; }; @@ -868,6 +874,7 @@ 11B359F28FAF97AD4F7F6424 /* NftPriceRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35B451378835F7F060012 /* NftPriceRecord.swift */; }; 11B359F4651EA254E5B0AD00 /* ManageAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35A38C734DF3157C84678 /* ManageAccountViewController.swift */; }; 11B359F73F1D626BF832977F /* BackupModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358967A086CFE9DBB152B /* BackupModule.swift */; }; + 11B359F926F72DF79E9245E3 /* CoinPriceListMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ADBD038830223A8375D /* CoinPriceListMode.swift */; }; 11B359FB94269D9076C396D4 /* NftAssetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350E1584E954D281FA87D /* NftAssetView.swift */; }; 11B359FBC96E5ED356519001 /* WalletTokenListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35136653741E9703E61DE /* WalletTokenListViewModel.swift */; }; 11B35A07ED63F869C0203244 /* CexDepositNetworkSelectViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35C227EDC2D4ED188A0FC /* CexDepositNetworkSelectViewController.swift */; }; @@ -974,6 +981,7 @@ 11B35B172B09C2017B389C07 /* AlertTitleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BF766EAC97E74CD620D /* AlertTitleCell.swift */; }; 11B35B1F63DBDEEF57043C97 /* PublicKeysService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358A294479046C42D2E6B /* PublicKeysService.swift */; }; 11B35B26C32F47DE6E83FC82 /* BlockchainTokensModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35BC10B98A0770A2AC342 /* BlockchainTokensModule.swift */; }; + 11B35B2E335C24608DE32B0A /* AppWidgetConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E298D53B8A2C2684119 /* AppWidgetConstants.swift */; }; 11B35B30F84E64131CD41C31 /* ReceiveSelectCoinViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B350575488360C1A598DF3 /* ReceiveSelectCoinViewModel.swift */; }; 11B35B3134F108B2DB60D80B /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ABF8159065957CD3EF8 /* SubscriptionManager.swift */; }; 11B35B36FB559CDEB1B496EC /* NftHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ECC6866F29A33129F06 /* NftHeaderView.swift */; }; @@ -1172,6 +1180,7 @@ 11B35D94B4E92789F681E293 /* Coin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35F3FBFB1F4BE93B796DF /* Coin.swift */; }; 11B35D95692647EB9F73D9DB /* CoinProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3534997B5CD413DBDB7C7 /* CoinProvider.swift */; }; 11B35D96A814579F37BAD3D0 /* CoinRankService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3513AC6560B9C37C342F3 /* CoinRankService.swift */; }; + 11B35DA145308F888592A7CF /* CoinPriceListMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35ADBD038830223A8375D /* CoinPriceListMode.swift */; }; 11B35DA1A83CE7E402309FE7 /* CreateAccountModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B358145A0D9F93ACBC0301 /* CreateAccountModule.swift */; }; 11B35DA4CB435537AD4148D7 /* NftCollectionAssetsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3573B3FE1FD8B476375E6 /* NftCollectionAssetsViewModel.swift */; }; 11B35DA5492B0C4EC7130A19 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3586FDC91E3742847B7E0 /* AppConfig.swift */; }; @@ -1373,6 +1382,7 @@ 11B35FC689D745FFBB3684C4 /* TokenType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3519AE11DC423E0D078E2 /* TokenType.swift */; }; 11B35FC6DE83EE46FB361756 /* CexWithdrawModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35507A989EA73EE5E8EA8 /* CexWithdrawModule.swift */; }; 11B35FD18C255E2C6D75F38A /* RestoreMnemonicHintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35CB288AF5A54B99A51E4 /* RestoreMnemonicHintView.swift */; }; + 11B35FD593B38EEEE5F18010 /* AppWidgetConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35E298D53B8A2C2684119 /* AppWidgetConstants.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 */; }; @@ -3150,6 +3160,7 @@ 11B3557DF76CFEBE7DA50D81 /* BottomGradientWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomGradientWrapper.swift; sourceTree = ""; }; 11B35588D5C27AD3673DEE2F /* AppIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = ""; }; 11B3558ACECAA1C886FA82C0 /* RecoveryPhraseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseViewController.swift; sourceTree = ""; }; + 11B3558D624AF040E9D102DF /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 11B35592753D3F2A9CCA5809 /* UnlinkWatchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnlinkWatchViewController.swift; sourceTree = ""; }; 11B355949F6D268EF1977DC9 /* ManageAccountViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageAccountViewModel.swift; sourceTree = ""; }; 11B3559E9511A2E053E00361 /* WatchPublicKeyService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchPublicKeyService.swift; sourceTree = ""; }; @@ -3394,10 +3405,12 @@ 11B35ABF8159065957CD3EF8 /* SubscriptionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = ""; }; 11B35AC2D01DF06DC50EAC6A /* HighlightedTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HighlightedTextView.swift; sourceTree = ""; }; 11B35AD211091A7C8619CEA2 /* CexAssetRecordStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CexAssetRecordStorage.swift; sourceTree = ""; }; + 11B35ADBD038830223A8375D /* CoinPriceListMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinPriceListMode.swift; sourceTree = ""; }; 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 = ""; }; + 11B35B0879F715C0777919AA /* WatchlistWidget.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistWidget.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 = ""; }; @@ -3531,6 +3544,7 @@ 11B35DFBFBF34277E7FC3325 /* ActivateSubscriptionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivateSubscriptionViewModel.swift; sourceTree = ""; }; 11B35E1B9C559545FC3E6226 /* ReceiveTokenViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveTokenViewModel.swift; sourceTree = ""; }; 11B35E255F6CA21FFA9E6B42 /* FavoriteCoinRecord_v_0_22.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteCoinRecord_v_0_22.swift; sourceTree = ""; }; + 11B35E298D53B8A2C2684119 /* AppWidgetConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppWidgetConstants.swift; sourceTree = ""; }; 11B35E2ACF02E2C35EFAE9FA /* NftMetadataService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftMetadataService.swift; sourceTree = ""; }; 11B35E2D539ACED30C947F2C /* RestoreBinanceService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreBinanceService.swift; sourceTree = ""; }; 11B35E343901BA7DE01181CB /* RestoreSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreSettingsViewModel.swift; sourceTree = ""; }; @@ -5433,6 +5447,8 @@ 11B3531363949F235A210921 /* ApiProvider.swift */, 11B357889F003A0B33D9DF27 /* PriceChangeType.swift */, 11B357A5569EAC7D20CD40B2 /* ValueFormatter.swift */, + 11B3558D624AF040E9D102DF /* Extensions.swift */, + 11B35ADBD038830223A8375D /* CoinPriceListMode.swift */, ); path = Misc; sourceTree = ""; @@ -8173,6 +8189,8 @@ 11B351F5E57874D4517F67B7 /* SingleCoinPriceWidget.swift */, 11B35693EA7B807D24C090E4 /* Misc */, 11B35AA425D946D470C85FC0 /* AppWidget.intentdefinition */, + 11B35B0879F715C0777919AA /* WatchlistWidget.swift */, + 11B35E298D53B8A2C2684119 /* AppWidgetConstants.swift */, ); path = Widget; sourceTree = ""; @@ -9902,6 +9920,7 @@ 11B35DDE363387B6E7A1D3B9 /* TabButtonStyle.swift in Sources */, 11B35BF002B002DE7B20DB0C /* SharedLocalStorage.swift in Sources */, 11B35CA7E33A993135B8A42D /* AppWidget.intentdefinition in Sources */, + 11B359A79743E965870AE8B6 /* AppWidgetConstants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -11242,6 +11261,7 @@ 11B3551F51D987A150C3BC26 /* TabButtonStyle.swift in Sources */, 11B35D5CEB75CD7626D6A612 /* SharedLocalStorage.swift in Sources */, 11B359CDBD4539989490C964 /* AppWidget.intentdefinition in Sources */, + 11B35FD593B38EEEE5F18010 /* AppWidgetConstants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -11268,6 +11288,10 @@ 11B358587D9C3A1F10EC15A6 /* PriceChangeType.swift in Sources */, 11B35530A9FC0972D8716C31 /* ValueFormatter.swift in Sources */, 11B3569D24EF634D1E91E437 /* AppWidget.intentdefinition in Sources */, + 11B3550548CB49D32EAC1DF5 /* WatchlistWidget.swift in Sources */, + 11B35287E46AFFBC47162F67 /* Extensions.swift in Sources */, + 11B35DA145308F888592A7CF /* CoinPriceListMode.swift in Sources */, + 11B3541E8CB5F0F743E9CDF3 /* AppWidgetConstants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -11304,6 +11328,10 @@ 11B35F0D313E455BCF24C42B /* PriceChangeType.swift in Sources */, 11B3518F3962FEA97AE6C7CD /* ValueFormatter.swift in Sources */, 11B3510E46A2954312E0B7C2 /* AppWidget.intentdefinition in Sources */, + 11B359BF322A7B912C778348 /* WatchlistWidget.swift in Sources */, + 11B35700253CCD66C4CCE354 /* Extensions.swift in Sources */, + 11B359F926F72DF79E9245E3 /* CoinPriceListMode.swift in Sources */, + 11B35B2E335C24608DE32B0A /* AppWidgetConstants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/UnstoppableWallet/UnstoppableWallet/Core/App.swift b/UnstoppableWallet/UnstoppableWallet/Core/App.swift index 899ce384e6..9128e2384c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/App.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/App.swift @@ -129,7 +129,8 @@ class App { let logRecordStorage = LogRecordStorage(dbPool: dbPool) logRecordManager = LogRecordManager(storage: logRecordStorage) - currencyManager = CurrencyManager(storage: SharedLocalStorage()) + let sharedLocalStorage = SharedLocalStorage() + currencyManager = CurrencyManager(storage: sharedLocalStorage) marketKit = try MarketKit.Kit.instance( hsApiBaseUrl: AppConfig.marketApiUrl, @@ -248,7 +249,7 @@ class App { feeRateProviderFactory = FeeRateProviderFactory() let favoriteCoinRecordStorage = FavoriteCoinRecordStorage(dbPool: dbPool) - favoritesManager = FavoritesManager(storage: favoriteCoinRecordStorage) + favoritesManager = FavoritesManager(storage: favoriteCoinRecordStorage, sharedStorage: sharedLocalStorage) let appVersionRecordStorage = AppVersionRecordStorage(dbPool: dbPool) appVersionStorage = AppVersionStorage(storage: appVersionRecordStorage) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Favorites/FavoritesManager.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Favorites/FavoritesManager.swift index 12d09d3e4d..26bffa0dea 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Favorites/FavoritesManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Favorites/FavoritesManager.swift @@ -1,20 +1,28 @@ -import RxSwift import RxCocoa +import RxSwift +import WidgetKit class FavoritesManager { private let storage: FavoriteCoinRecordStorage + private let sharedStorage: SharedLocalStorage - private let coinUidsUpdatedRelay = PublishRelay<()>() + private let coinUidsUpdatedRelay = PublishRelay() - init(storage: FavoriteCoinRecordStorage) { + init(storage: FavoriteCoinRecordStorage, sharedStorage: SharedLocalStorage) { self.storage = storage + self.sharedStorage = sharedStorage + + syncSharedStorage() } + private func syncSharedStorage() { + sharedStorage.set(value: allCoinUids, for: AppWidgetConstants.keyFavoriteCoinUids) + WidgetCenter.shared.reloadTimelines(ofKind: AppWidgetConstants.watchlistWidgetKind) + } } extension FavoritesManager { - - var coinUidsUpdatedObservable: Observable<()> { + var coinUidsUpdatedObservable: Observable { coinUidsUpdatedRelay.asObservable() } @@ -25,11 +33,13 @@ extension FavoritesManager { func add(coinUid: String) { storage.save(favoriteCoinRecord: FavoriteCoinRecord(coinUid: coinUid)) coinUidsUpdatedRelay.accept(()) + syncSharedStorage() } func add(coinUids: [String]) { storage.save(favoriteCoinRecords: coinUids.map { FavoriteCoinRecord(coinUid: $0) }) coinUidsUpdatedRelay.accept(()) + syncSharedStorage() } func removeAll() { @@ -39,10 +49,10 @@ extension FavoritesManager { func remove(coinUid: String) { storage.deleteFavoriteCoinRecord(coinUid: coinUid) coinUidsUpdatedRelay.accept(()) + syncSharedStorage() } func isFavorite(coinUid: String) -> Bool { storage.favoriteCoinRecordExists(coinUid: coinUid) } - } diff --git a/UnstoppableWallet/Widget/AppWidgetBundle.swift b/UnstoppableWallet/Widget/AppWidgetBundle.swift index 48cad1217c..aad71fc1ab 100644 --- a/UnstoppableWallet/Widget/AppWidgetBundle.swift +++ b/UnstoppableWallet/Widget/AppWidgetBundle.swift @@ -6,5 +6,6 @@ struct AppWidgetBundle: WidgetBundle { var body: some Widget { SingleCoinPriceWidget() TopCoinsWidget() + WatchlistWidget() } } diff --git a/UnstoppableWallet/Widget/AppWidgetConstants.swift b/UnstoppableWallet/Widget/AppWidgetConstants.swift new file mode 100644 index 0000000000..5fe71776b2 --- /dev/null +++ b/UnstoppableWallet/Widget/AppWidgetConstants.swift @@ -0,0 +1,7 @@ +enum AppWidgetConstants { + static let singleCoinPriceWidgetKind: String = "io.horizontalsystems.unstoppable.SingleCoinPriceWidget" + static let topCoinsWidgetKind: String = "io.horizontalsystems.unstoppable.TopCoinsWidget" + static let watchlistWidgetKind: String = "io.horizontalsystems.unstoppable.WatchlistWidget" + + static let keyFavoriteCoinUids = "favorite_coin_uids" +} diff --git a/UnstoppableWallet/Widget/Assets.xcassets/rate_48.imageset/Contents.json b/UnstoppableWallet/Widget/Assets.xcassets/rate_48.imageset/Contents.json new file mode 100644 index 0000000000..f4d232b894 --- /dev/null +++ b/UnstoppableWallet/Widget/Assets.xcassets/rate_48.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "rate@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "rate@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/UnstoppableWallet/Widget/Assets.xcassets/rate_48.imageset/rate@2x.png b/UnstoppableWallet/Widget/Assets.xcassets/rate_48.imageset/rate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe8fdec0fa704eb115da8c358478592461d10a7e GIT binary patch literal 2221 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 literal 0 HcmV?d00001 diff --git a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListEntry.swift b/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListEntry.swift index 2501ddf929..01862bce0e 100644 --- a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListEntry.swift +++ b/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListEntry.swift @@ -4,8 +4,9 @@ import WidgetKit struct CoinPriceListEntry: TimelineEntry { let date: Date - let title: String + let mode: CoinPriceListMode let sortType: String + let maxItemCount: Int let items: [Item] struct Item { diff --git a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListProvider.swift b/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListProvider.swift index 3aa51faf61..8954e780d6 100644 --- a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListProvider.swift +++ b/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListProvider.swift @@ -3,6 +3,8 @@ import SwiftUI import WidgetKit struct CoinPriceListProvider: IntentTimelineProvider { + let mode: CoinPriceListMode + func placeholder(in context: Context) -> CoinPriceListEntry { let count: Int @@ -13,8 +15,9 @@ struct CoinPriceListProvider: IntentTimelineProvider { return CoinPriceListEntry( date: Date(), - title: "Top Coins", + mode: mode, sortType: "Highest Cap", + maxItemCount: count, items: (1 ... count).map { index in CoinPriceListEntry.Item( uid: "coin\(index)", @@ -48,7 +51,8 @@ struct CoinPriceListProvider: IntentTimelineProvider { } private func fetch(sortType: SortType, family: WidgetFamily) async throws -> CoinPriceListEntry { - let currency = CurrencyManager(storage: SharedLocalStorage()).baseCurrency + let storage = SharedLocalStorage() + let currency = CurrencyManager(storage: storage).baseCurrency let apiProvider = ApiProvider(baseUrl: "https://api-dev.blocksdecoded.com") let listType: ApiProvider.ListType @@ -71,24 +75,35 @@ struct CoinPriceListProvider: IntentTimelineProvider { default: limit = 6 } - let coins = try await apiProvider.listCoins(type: listType, order: listOrder, limit: limit, currencyCode: currency.code) + let coins: [Coin] + + switch mode { + case .topCoins: + coins = try await apiProvider.listCoins(type: listType, order: listOrder, limit: limit, currencyCode: currency.code) + case .watchlist: + let coinUids: [String]? = storage.value(for: AppWidgetConstants.keyFavoriteCoinUids) + + if let coinUids, !coinUids.isEmpty { + coins = try await apiProvider.listCoins(uids: coinUids, type: listType, order: listOrder, limit: limit, currencyCode: currency.code) + } else { + coins = [] + } + } return CoinPriceListEntry( date: Date(), - title: "Top Coins", + mode: mode, sortType: title(sortType: sortType), + maxItemCount: limit, items: coins.map { coin in - let iconUrl = "https://cdn.blocksdecoded.com/coin-icons/32px/\(coin.uid)@3x.png" - let coinIcon = URL(string: iconUrl).flatMap { try? Data(contentsOf: $0) }.flatMap { UIImage(data: $0) }.map { Image(uiImage: $0) } - - return CoinPriceListEntry.Item( + CoinPriceListEntry.Item( uid: coin.uid, - icon: coinIcon, + icon: coin.image, code: coin.code, name: coin.name, - price: coin.price.flatMap { ValueFormatter.format(currency: currency, value: $0) } ?? "n/a", - priceChange: coin.priceChange24h.flatMap { ValueFormatter.format(percentValue: $0) } ?? "n/a", - priceChangeType: coin.priceChange24h.map { $0 >= 0 ? .up : .down } ?? .unknown + price: coin.formattedPrice(currency: currency), + priceChange: coin.formattedPriceChange, + priceChangeType: coin.priceChangeType ) } ) diff --git a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListView.swift b/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListView.swift index f7144edbcc..59e78a24cd 100644 --- a/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListView.swift +++ b/UnstoppableWallet/Widget/CoinPriceList/CoinPriceListView.swift @@ -16,46 +16,34 @@ struct CoinPriceListView: View { } @ViewBuilder private func smallView() -> some View { - ListSection { - ForEach(entry.items, id: \.uid) { item in - HStack(spacing: .margin8) { - icon(image: item.icon) - - VStack(spacing: 1) { - Text(item.price) - .frame(maxWidth: .infinity, alignment: .leading) - .font(.themeSubhead1) - .foregroundColor(.themeLeah) - Text(item.priceChange) - .frame(maxWidth: .infinity, alignment: .leading) - .font(.themeCaption) - .foregroundColor(item.priceChangeType.color) - } + list(verticalPadding: .margin4) { item in + HStack(spacing: .margin8) { + icon(image: item.icon) + + VStack(spacing: 1) { + Text(item.price) + .frame(maxWidth: .infinity, alignment: .leading) + .font(.themeSubhead1) + .foregroundColor(.themeLeah) + Text(item.priceChange) + .frame(maxWidth: .infinity, alignment: .leading) + .font(.themeCaption) + .foregroundColor(item.priceChangeType.color) } - .padding(.horizontal, .margin16) - .frame(maxHeight: .infinity) } } - .listStyle(.transparent) - .frame(maxHeight: .infinity) - .padding(.vertical, .margin4) } @ViewBuilder private func mediumView() -> some View { - ListSection { - ForEach(entry.items, id: \.uid) { item in - row(item: item) - } + list(verticalPadding: .margin4) { item in + row(item: item) } - .listStyle(.transparent) - .frame(maxHeight: .infinity) - .padding(.vertical, .margin4) } @ViewBuilder private func largeView() -> some View { VStack(spacing: 0) { HStack(spacing: .margin16) { - Text(entry.title) + Text(entry.mode.title) .frame(maxWidth: .infinity, alignment: .leading) .font(.themeSubhead1) .foregroundColor(.themeLeah) @@ -69,27 +57,56 @@ struct CoinPriceListView: View { HorizontalDivider() - ListSection { - ForEach(entry.items, id: \.uid) { item in - row(item: item) - } + list(verticalPadding: 0) { item in + row(item: item) } - .listStyle(.transparent) - .frame(maxHeight: .infinity) } .padding(.vertical, .margin4) } - @ViewBuilder private func icon(image: Image?) -> some View { - if let image = image { - image - .resizable() - .scaledToFit() - .frame(width: .iconSize32, height: .iconSize32) + @ViewBuilder private func list(verticalPadding: CGFloat, rowBuilder: @escaping (CoinPriceListEntry.Item) -> some View) -> some View { + if entry.mode.isWatchlist, entry.items.isEmpty { + VStack(spacing: .margin16) { + switch family { + case .systemLarge: + ZStack { + Circle() + .fill(Color.themeRaina) + .frame(width: 100, height: 100) + + Image("rate_48") + .renderingMode(.template) + .foregroundColor(.themeGray) + } + default: + EmptyView() + } + + Text("Your watchlist is empty.") + .multilineTextAlignment(.center) + .font(.themeSubhead2) + .foregroundColor(.themeGray) + } + .frame(maxHeight: .infinity) + .padding(.margin16) } else { - Circle() - .fill(Color.themeGray) - .frame(width: .iconSize32, height: .iconSize32) + GeometryReader { proxy in + ListSection { + ForEach(entry.items, id: \.uid) { item in + rowBuilder(item) + .padding(.horizontal, .margin16) + .frame(maxHeight: .infinity) + .frame(maxHeight: proxy.size.height / CGFloat(entry.maxItemCount)) + } + + if entry.items.count < entry.maxItemCount { + Spacer() + } + } + .listStyle(.transparent) + } + .frame(maxHeight: .infinity) + .padding(.vertical, verticalPadding) } } @@ -122,7 +139,18 @@ struct CoinPriceListView: View { } } } - .padding(.horizontal, .margin16) - .frame(maxHeight: .infinity) + } + + @ViewBuilder private func icon(image: Image?) -> some View { + if let image = image { + image + .resizable() + .scaledToFit() + .frame(width: .iconSize32, height: .iconSize32) + } else { + Circle() + .fill(Color.themeGray) + .frame(width: .iconSize32, height: .iconSize32) + } } } diff --git a/UnstoppableWallet/Widget/Misc/ApiProvider.swift b/UnstoppableWallet/Widget/Misc/ApiProvider.swift index 7d29e766af..51c21695f9 100644 --- a/UnstoppableWallet/Widget/Misc/ApiProvider.swift +++ b/UnstoppableWallet/Widget/Misc/ApiProvider.swift @@ -36,13 +36,17 @@ class ApiProvider { return try await networkManager.fetch(url: "\(baseUrl)/v1/coins", method: .get, parameters: parameters, headers: headers) } - func listCoins(type: ListType, order: ListOrder, limit: Int, currencyCode: String) async throws -> [Coin] { - let parameters: Parameters = [ + func listCoins(uids: [String]? = nil, type: ListType, order: ListOrder, limit: Int, currencyCode: String) async throws -> [Coin] { + var parameters: Parameters = [ "order": order.rawValue, "limit": limit, "currency": currencyCode.lowercased(), ] + if let uids { + parameters["uids"] = uids.joined(separator: ",") + } + return try await networkManager.fetch(url: "\(baseUrl)/v1/coins/top-movers-by/\(type.rawValue)", method: .get, parameters: parameters, headers: headers) } diff --git a/UnstoppableWallet/Widget/Misc/CoinPriceListMode.swift b/UnstoppableWallet/Widget/Misc/CoinPriceListMode.swift new file mode 100644 index 0000000000..f1bd7d06a5 --- /dev/null +++ b/UnstoppableWallet/Widget/Misc/CoinPriceListMode.swift @@ -0,0 +1,18 @@ +enum CoinPriceListMode { + case topCoins + case watchlist + + var title: String { + switch self { + case .topCoins: return "Top Coins" + case .watchlist: return "Watchlist" + } + } + + var isWatchlist: Bool { + switch self { + case .watchlist: return true + default: return false + } + } +} diff --git a/UnstoppableWallet/Widget/Misc/Extensions.swift b/UnstoppableWallet/Widget/Misc/Extensions.swift new file mode 100644 index 0000000000..f321cec50b --- /dev/null +++ b/UnstoppableWallet/Widget/Misc/Extensions.swift @@ -0,0 +1,29 @@ +import SwiftUI + +extension Coin { + var image: Image? { + let iconUrl = "https://cdn.blocksdecoded.com/coin-icons/32px/\(uid)@3x.png" + + guard let url = URL(string: iconUrl) else { return nil } + guard let data = try? Data(contentsOf: url) else { return nil } + guard let uiImage = UIImage(data: data) else { return nil } + + return Image(uiImage: uiImage) + } + + func formattedPrice(currency: Currency) -> String { + price.flatMap { ValueFormatter.format(currency: currency, value: $0) } ?? "n/a" + } + + var formattedPriceChange: String { + priceChange24h.flatMap { ValueFormatter.format(percentValue: $0) } ?? "n/a" + } + + var priceChangeType: PriceChangeType { + guard let priceChange24h else { + return .unknown + } + + return priceChange24h >= 0 ? .up : .down + } +} diff --git a/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceEntry.swift b/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceEntry.swift index de06d1dfa9..21cbf6942f 100644 --- a/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceEntry.swift +++ b/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceEntry.swift @@ -4,9 +4,9 @@ import WidgetKit struct SingleCoinPriceEntry: TimelineEntry { let date: Date - let coinUid: String - let coinIcon: Image? - let coinCode: String + let uid: String + let icon: Image? + let code: String let price: String let priceChange: String let priceChangeType: PriceChangeType diff --git a/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceProvider.swift b/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceProvider.swift index 204b8c7e36..d9071efafb 100644 --- a/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceProvider.swift +++ b/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceProvider.swift @@ -8,9 +8,9 @@ struct SingleCoinPriceProvider: IntentTimelineProvider { func placeholder(in _: Context) -> SingleCoinPriceEntry { SingleCoinPriceEntry( date: Date(), - coinUid: fallbackCoinUid, - coinIcon: nil, - coinCode: "BTC", + uid: fallbackCoinUid, + icon: nil, + code: "BTC", price: "$30000", priceChange: "2.45", priceChangeType: .unknown, @@ -44,9 +44,6 @@ struct SingleCoinPriceProvider: IntentTimelineProvider { let coin = try await apiProvider.coinWithPrice(uid: coinUid, currencyCode: currency.code) - let iconUrl = "https://cdn.blocksdecoded.com/coin-icons/32px/\(coin.uid)@3x.png" - let coinIcon = URL(string: iconUrl).flatMap { try? Data(contentsOf: $0) }.flatMap { UIImage(data: $0) }.map { Image(uiImage: $0) } - var chartPoints: [SingleCoinPriceEntry.ChartPoint]? if let points = try? await apiProvider.coinPriceChart(coinUid: coinUid, currencyCode: currency.code) { @@ -64,12 +61,12 @@ struct SingleCoinPriceProvider: IntentTimelineProvider { return SingleCoinPriceEntry( date: Date(), - coinUid: coin.uid, - coinIcon: coinIcon, - coinCode: coin.code, - price: coin.price.flatMap { ValueFormatter.format(currency: currency, value: $0) } ?? "n/a", - priceChange: coin.priceChange24h.flatMap { ValueFormatter.format(percentValue: $0) } ?? "n/a", - priceChangeType: coin.priceChange24h.map { $0 >= 0 ? .up : .down } ?? .unknown, + uid: coin.uid, + icon: coin.image, + code: coin.code, + price: coin.formattedPrice(currency: currency), + priceChange: coin.formattedPriceChange, + priceChangeType: coin.priceChangeType, chartPoints: chartPoints ) } diff --git a/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceView.swift b/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceView.swift index cadc50c2c7..7da0fc9145 100644 --- a/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceView.swift +++ b/UnstoppableWallet/Widget/SingleCoinPrice/SingleCoinPriceView.swift @@ -7,7 +7,7 @@ struct SingleCoinPriceView: View { var body: some View { VStack(alignment: .leading, spacing: .margin8) { HStack(spacing: .margin8) { - if let coinIcon = entry.coinIcon { + if let coinIcon = entry.icon { coinIcon .resizable() .scaledToFit() @@ -18,7 +18,7 @@ struct SingleCoinPriceView: View { .frame(width: .iconSize32, height: .iconSize32) } - Text(entry.coinCode.uppercased()) + Text(entry.code.uppercased()) .frame(maxWidth: .infinity, alignment: .leading) .foregroundColor(.themeLeah) .font(.themeSubhead1) diff --git a/UnstoppableWallet/Widget/SingleCoinPriceWidget.swift b/UnstoppableWallet/Widget/SingleCoinPriceWidget.swift index 7394310eaf..b1641e7811 100644 --- a/UnstoppableWallet/Widget/SingleCoinPriceWidget.swift +++ b/UnstoppableWallet/Widget/SingleCoinPriceWidget.swift @@ -3,11 +3,9 @@ import SwiftUI import WidgetKit struct SingleCoinPriceWidget: Widget { - let kind: String = "io.horizontalsystems.unstoppable.SingleCoinPriceWidget" - var body: some WidgetConfiguration { IntentConfiguration( - kind: kind, + kind: AppWidgetConstants.singleCoinPriceWidgetKind, intent: SingleCoinPriceIntent.self, provider: SingleCoinPriceProvider() ) { entry in diff --git a/UnstoppableWallet/Widget/TopCoinsWidget.swift b/UnstoppableWallet/Widget/TopCoinsWidget.swift index bb452e6003..1fe07deee2 100644 --- a/UnstoppableWallet/Widget/TopCoinsWidget.swift +++ b/UnstoppableWallet/Widget/TopCoinsWidget.swift @@ -3,13 +3,11 @@ import SwiftUI import WidgetKit struct TopCoinsWidget: Widget { - let kind: String = "io.horizontalsystems.unstoppable.TopCoinsWidget" - var body: some WidgetConfiguration { IntentConfiguration( - kind: kind, + kind: AppWidgetConstants.topCoinsWidgetKind, intent: CoinPriceListIntent.self, - provider: CoinPriceListProvider() + provider: CoinPriceListProvider(mode: .topCoins) ) { entry in if #available(iOS 17.0, *) { CoinPriceListView(entry: entry) diff --git a/UnstoppableWallet/Widget/WatchlistWidget.swift b/UnstoppableWallet/Widget/WatchlistWidget.swift new file mode 100644 index 0000000000..c64df83d7a --- /dev/null +++ b/UnstoppableWallet/Widget/WatchlistWidget.swift @@ -0,0 +1,30 @@ +import Foundation +import SwiftUI +import WidgetKit + +struct WatchlistWidget: Widget { + var body: some WidgetConfiguration { + IntentConfiguration( + kind: AppWidgetConstants.watchlistWidgetKind, + intent: CoinPriceListIntent.self, + provider: CoinPriceListProvider(mode: .watchlist) + ) { entry in + if #available(iOS 17.0, *) { + CoinPriceListView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } else { + CoinPriceListView(entry: entry) + .background() + } + } + .contentMarginsDisabled() + .configurationDisplayName("Watchlist") + .description("Displays price coins in watchlist.") + + .supportedFamilies([ + .systemSmall, + .systemMedium, + .systemLarge, + ]) + } +}