diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Rank/RankView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Rank/RankView.swift index b10d3d1c1e..81f2ccd678 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Rank/RankView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Rank/RankView.swift @@ -28,13 +28,17 @@ struct RankView: View { Spacer() } case let .loaded(items): - ThemeList(bottomSpacing: .margin16) { - header() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - - list(items: items) + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + list(items: items) + } + .onChange(of: viewModel.sortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.timePeriod) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } case .failed: VStack(spacing: 0) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesView.swift index 308608a94a..c39a89a1d9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesView.swift @@ -16,19 +16,23 @@ struct CoinTreasuriesView: View { switch viewModel.state { case .loading: VStack(spacing: 0) { - header() + header(disabled: true) loadingList() } case let .loaded(treasuries): VStack(spacing: 0) { header() - ThemeList(bottomSpacing: .margin32) { - list(treasuries: treasuries) - footer() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin32, invisibleTopView: true) { + list(treasuries: treasuries) + footer() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + } + .onChange(of: viewModel.filter) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.sortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } } case .failed: @@ -41,7 +45,7 @@ struct CoinTreasuriesView: View { } } - @ViewBuilder private func header() -> some View { + @ViewBuilder private func header(disabled: Bool = false) -> some View { HorizontalDivider(color: .themeSteel10) HStack { HStack { @@ -51,42 +55,46 @@ struct CoinTreasuriesView: View { Text(viewModel.filter.title) } .buttonStyle(SecondaryButtonStyle(style: .transparent, rightAccessory: .dropDown)) + .disabled(disabled) } .alert( isPresented: $filterSelectorPresented, title: "coin_analytics.treasuries.filters".localized, - viewItems: viewModel.filters.map { .init(text: $0.title, selected: viewModel.filter == $0) }, + viewItems: CoinTreasuriesViewModel.Filter.allCases.map { .init(text: $0.title, selected: viewModel.filter == $0) }, onTap: { index in guard let index else { return } - viewModel.filter = viewModel.filters[index] + viewModel.filter = CoinTreasuriesViewModel.Filter.allCases[index] } ) Spacer() Button(action: { - viewModel.toggleSortBy() + viewModel.sortOrder.toggle() }) { - Image(viewModel.orderedAscending ? "sort_l2h_20" : "sort_h2l_20").renderingMode(.template) + sortIcon().renderingMode(.template) } .buttonStyle(SecondaryCircleButtonStyle(style: .default)) .padding(.trailing, .margin16) + .disabled(disabled) } .padding(.vertical, .margin8) } @ViewBuilder private func list(treasuries: [CoinTreasury]) -> some View { ListForEach(treasuries) { treasury in - itemContent( - logoUrl: treasury.fundLogoUrl, - fund: treasury.fund, - amount: ValueFormatter.instance.formatShort(value: treasury.amount, decimalCount: 8, symbol: viewModel.coinCode) ?? "---", - country: treasury.country, - amountInCurrency: ValueFormatter.instance.formatShort(currency: viewModel.currency, value: treasury.amountInCurrency) ?? "---" - ) + ListRow { + itemContent( + imageUrl: URL(string: treasury.fundLogoUrl), + fund: treasury.fund, + amount: ValueFormatter.instance.formatShort(value: treasury.amount, decimalCount: 8, symbol: viewModel.coinCode) ?? "---", + country: treasury.country, + amountInCurrency: ValueFormatter.instance.formatShort(currency: viewModel.currency, value: treasury.amountInCurrency) ?? "---" + ) + } } } @@ -102,11 +110,11 @@ struct CoinTreasuriesView: View { ThemeList(Array(0 ... 10)) { _ in ListRow { itemContent( - logoUrl: nil, - fund: "", - amount: "", - country: "", - amountInCurrency: "" + imageUrl: nil, + fund: "Unstoppable", + amount: "123.45 BTC", + country: "KG", + amountInCurrency: "$123.45" ) .redacted() } @@ -114,27 +122,32 @@ struct CoinTreasuriesView: View { .simultaneousGesture(DragGesture(minimumDistance: 0), including: .all) } - @ViewBuilder private func itemContent(logoUrl: String?, fund: String, amount: String, country: String, amountInCurrency: String) -> some View { - ListRow { - if let url = logoUrl.flatMap({ URL(string: $0) }) { - KFImage.url(url) - .resizable() - .frame(width: .iconSize32, height: .iconSize32) - } + @ViewBuilder private func itemContent(imageUrl: URL?, fund: String, amount: String, country: String, amountInCurrency: String) -> some View { + KFImage.url(imageUrl) + .resizable() + .placeholder { RoundedRectangle(cornerRadius: .cornerRadius8).fill(Color.themeSteel20) } + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius8)) + .frame(width: .iconSize32, height: .iconSize32) - VStack(spacing: 1) { - HStack(spacing: .margin8) { - Text(fund).textBody() - Spacer() - Text(amount).textBody() - } + VStack(spacing: 1) { + HStack(spacing: .margin8) { + Text(fund).textBody() + Spacer() + Text(amount).textBody() + } - HStack(spacing: .margin8) { - Text(country).textSubhead2() - Spacer() - Text(amountInCurrency).textSubhead2(color: .themeJacob) - } + HStack(spacing: .margin8) { + Text(country).textSubhead2() + Spacer() + Text(amountInCurrency).textSubhead2(color: .themeJacob) } } } + + private func sortIcon() -> Image { + switch viewModel.sortOrder { + case .asc: return Image("sort_l2h_20") + case .desc: return Image("sort_h2l_20") + } + } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesViewModel.swift index 43ab4f23d5..beb8d9e417 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Treasuries/CoinTreasuriesViewModel.swift @@ -7,22 +7,23 @@ class CoinTreasuriesViewModel: ObservableObject { private let coin: Coin private let marketKit = App.shared.marketKit private let currencyManager = App.shared.currencyManager - - private var cancellables = Set() private var tasks = Set() private var internalState: State = .loading { didSet { - DispatchQueue.main.async { [weak self] in - self?.syncState() - } + syncState() } } @Published var state: State = .loading - @Published var orderedAscending: Bool = false - var filter: Filter = .all { + @Published var filter: Filter = .all { + didSet { + syncState() + } + } + + @Published var sortOrder: MarketModule.SortOrder = .desc { didSet { syncState() } @@ -44,15 +45,17 @@ class CoinTreasuriesViewModel: ObservableObject { Task { [weak self, marketKit, coin, currencyManager] in do { let treasuries = try await marketKit.treasuries(coinUid: coin.uid, currencyCode: currencyManager.baseCurrency.code) - DispatchQueue.main.async { [weak self] in + + await MainActor.run { [weak self] in self?.internalState = .loaded(treasuries) } } catch { - DispatchQueue.main.async { [weak self] in + await MainActor.run { [weak self] in self?.internalState = .failed(error) } } - }.store(in: &tasks) + } + .store(in: &tasks) } private func syncState() { @@ -70,7 +73,10 @@ class CoinTreasuriesViewModel: ObservableObject { } } .sorted { lhsTreasury, rhsTreasury in - orderedAscending ? lhsTreasury.amount < rhsTreasury.amount : lhsTreasury.amount > rhsTreasury.amount + switch sortOrder { + case .asc: lhsTreasury.amount < rhsTreasury.amount + case .desc: lhsTreasury.amount > rhsTreasury.amount + } } state = .loaded(treasuries) @@ -89,15 +95,6 @@ extension CoinTreasuriesViewModel { coin.code } - var filters: [Filter] { - Filter.allCases - } - - func toggleSortBy() { - orderedAscending = !orderedAscending - syncState() - } - func refresh() async { sync() } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchResultsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchResultsView.swift index 11ecdd018c..f10fc9465a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchResultsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/AdvancedSearch/MarketAdvancedSearchResultsView.swift @@ -21,21 +21,24 @@ struct MarketAdvancedSearchResultsView: View { VStack(spacing: 0) { header() - ThemeList(viewModel.marketInfos, bottomSpacing: .margin16) { marketInfo in - let coin = marketInfo.fullCoin.coin + ScrollViewReader { proxy in + ThemeList(viewModel.marketInfos, bottomSpacing: .margin16, invisibleTopView: true) { marketInfo in + let coin = marketInfo.fullCoin.coin - ClickableRow(action: { - presentedFullCoin = marketInfo.fullCoin - }) { - itemContent( - coin: coin, - marketCap: marketInfo.marketCap, - price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, - rank: marketInfo.marketCapRank, - diff: marketInfo.priceChangeValue(timePeriod: viewModel.timePeriod) - ) + ClickableRow(action: { + presentedFullCoin = marketInfo.fullCoin + }) { + itemContent( + coin: coin, + marketCap: marketInfo.marketCap, + price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + rank: marketInfo.marketCapRank, + diff: marketInfo.priceChangeValue(timePeriod: viewModel.timePeriod) + ) + } + .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) } - .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) + .onChange(of: viewModel.sortBy) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Coins/MarketCoinsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Coins/MarketCoinsView.swift index 1a5e647b51..fea10d3364 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Coins/MarketCoinsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Coins/MarketCoinsView.swift @@ -108,24 +108,29 @@ struct MarketCoinsView: View { } @ViewBuilder private func list(marketInfos: [MarketInfo]) -> some View { - ThemeList(marketInfos) { marketInfo in - let coin = marketInfo.fullCoin.coin + ScrollViewReader { proxy in + ThemeList(marketInfos, invisibleTopView: true) { marketInfo in + let coin = marketInfo.fullCoin.coin - ClickableRow(action: { - presentedFullCoin = marketInfo.fullCoin - }) { - itemContent( - coin: coin, - marketCap: marketInfo.marketCap, - price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, - rank: marketInfo.marketCapRank, - diff: marketInfo.priceChangeValue(timePeriod: viewModel.timePeriod) - ) + ClickableRow(action: { + presentedFullCoin = marketInfo.fullCoin + }) { + itemContent( + coin: coin, + marketCap: marketInfo.marketCap, + price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + rank: marketInfo.marketCapRank, + diff: marketInfo.priceChangeValue(timePeriod: viewModel.timePeriod) + ) + } + .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) } - .watchlistSwipeActions(viewModel: watchlistViewModel, coinUid: coin.uid) - } - .refreshable { - await viewModel.refresh() + .refreshable { + await viewModel.refresh() + } + .onChange(of: viewModel.sortBy) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.top) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.timePeriod) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift index f003baba9b..d9c183fcd6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift @@ -28,18 +28,22 @@ struct MarketEtfView: View { Spacer() } case let .loaded(etfs): - ThemeList(bottomSpacing: .margin16) { - header() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - - chart() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - - list(etfs: etfs) + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + chart() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + list(etfs: etfs) + } + .onChange(of: viewModel.sortBy) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.timePeriod) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } case .failed: VStack(spacing: 0) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCap/MarketMarketCapView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCap/MarketMarketCapView.swift index 107103b17f..96bd014e1a 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCap/MarketMarketCapView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketCap/MarketMarketCapView.swift @@ -29,18 +29,21 @@ struct MarketMarketCapView: View { Spacer() } case let .loaded(marketInfos): - ThemeList(bottomSpacing: .margin16) { - header() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - - chart() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - - list(marketInfos: marketInfos) + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + chart() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + list(marketInfos: marketInfos) + } + .onChange(of: viewModel.sortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } case .failed: VStack(spacing: 0) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Pairs/MarketPairsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Pairs/MarketPairsView.swift index ef2b4104f7..577e041874 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Pairs/MarketPairsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Pairs/MarketPairsView.swift @@ -45,28 +45,31 @@ struct MarketPairsView: View { } @ViewBuilder private func list(pairs: [MarketPair]) -> some View { - ThemeList(pairs) { pair in - ClickableRow(action: { - if let tradeUrl = pair.tradeUrl { - UrlManager.open(url: tradeUrl) - stat(page: .markets, section: .pairs, event: .open(page: .externalMarketPair)) + ScrollViewReader { proxy in + ThemeList(pairs, invisibleTopView: true) { pair in + ClickableRow(action: { + if let tradeUrl = pair.tradeUrl { + UrlManager.open(url: tradeUrl) + stat(page: .markets, section: .pairs, event: .open(page: .externalMarketPair)) + } + }) { + itemContent( + baseCoin: pair.baseCoin, + targetCoin: pair.targetCoin, + base: pair.base, + target: pair.target, + volume: pair.volume.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + marketName: pair.marketName, + rank: pair.rank, + price: pair.price.flatMap { ValueFormatter.instance.formatShort(value: $0, decimalCount: 8, symbol: pair.target) } ?? "n/a".localized + ) } - }) { - itemContent( - baseCoin: pair.baseCoin, - targetCoin: pair.targetCoin, - base: pair.base, - target: pair.target, - volume: pair.volume.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) } ?? "n/a".localized, - marketName: pair.marketName, - rank: pair.rank, - price: pair.price.flatMap { ValueFormatter.instance.formatShort(value: $0, decimalCount: 8, symbol: pair.target) } ?? "n/a".localized - ) } - } - .themeListStyle(.transparent) - .refreshable { - await viewModel.refresh() + .themeListStyle(.transparent) + .refreshable { + await viewModel.refresh() + } + .onChange(of: viewModel.volumeSortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platform/MarketPlatformViewNew.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platform/MarketPlatformViewNew.swift index 383da89e54..ff7bd08ea5 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platform/MarketPlatformViewNew.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platform/MarketPlatformViewNew.swift @@ -30,17 +30,20 @@ struct MarketPlatformViewNew: View { Spacer() } case let .loaded(marketInfos): - ThemeList(bottomSpacing: .margin16) { - header() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - chart() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - - list(marketInfos: marketInfos) + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + chart() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + list(marketInfos: marketInfos) + } + .onChange(of: viewModel.sortBy) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } case .failed: VStack(spacing: 0) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platforms/MarketPlatformsView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platforms/MarketPlatformsView.swift index abe7c700e3..56376d89c9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platforms/MarketPlatformsView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Platforms/MarketPlatformsView.swift @@ -91,26 +91,30 @@ struct MarketPlatformsView: View { } @ViewBuilder private func list(platforms: [TopPlatform]) -> some View { - ThemeList(platforms) { platform in - ClickableRow(action: { - presentedPlatform = platform - }) { - let blockchain = platform.blockchain - - itemContent( - imageUrl: URL(string: blockchain.type.imageUrl), - name: blockchain.name, - marketCap: platform.marketCap.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) } ?? "n/a".localized, - protocolsCount: platform.protocolsCount, - rank: platform.rank, - rankChange: platform.rank.flatMap { rank in platform.ranks[viewModel.timePeriod].map { $0 - rank } }, - diff: platform.changes[viewModel.timePeriod] - ) + ScrollViewReader { proxy in + ThemeList(platforms, invisibleTopView: true) { platform in + ClickableRow(action: { + presentedPlatform = platform + }) { + let blockchain = platform.blockchain + + itemContent( + imageUrl: URL(string: blockchain.type.imageUrl), + name: blockchain.name, + marketCap: platform.marketCap.flatMap { ValueFormatter.instance.formatShort(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + protocolsCount: platform.protocolsCount, + rank: platform.rank, + rankChange: platform.rank.flatMap { rank in platform.ranks[viewModel.timePeriod].map { $0 - rank } }, + diff: platform.changes[viewModel.timePeriod] + ) + } } - } - .themeListStyle(.transparent) - .refreshable { - await viewModel.refresh() + .themeListStyle(.transparent) + .refreshable { + await viewModel.refresh() + } + .onChange(of: viewModel.sortBy) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.timePeriod) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tvl/MarketTvlView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tvl/MarketTvlView.swift index edc7ec49f1..0624a1a47b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tvl/MarketTvlView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Tvl/MarketTvlView.swift @@ -29,17 +29,21 @@ struct MarketTvlView: View { Spacer() } case let .loaded(defiCoins): - ThemeList(bottomSpacing: .margin16) { - header() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - chart() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + chart() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) - list(defiCoins: defiCoins) + list(defiCoins: defiCoins) + } + .onChange(of: viewModel.platforms) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } + .onChange(of: viewModel.sortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } case .failed: VStack(spacing: 0) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Volume/MarketVolumeView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Volume/MarketVolumeView.swift index db005ca870..c2bda31409 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Volume/MarketVolumeView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Volume/MarketVolumeView.swift @@ -29,18 +29,21 @@ struct MarketVolumeView: View { Spacer() } case let .loaded(marketInfos): - ThemeList(bottomSpacing: .margin16) { - header() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - - chart() - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) - - list(marketInfos: marketInfos) + ScrollViewReader { proxy in + ThemeList(bottomSpacing: .margin16, invisibleTopView: true) { + header() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + chart() + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + + list(marketInfos: marketInfos) + } + .onChange(of: viewModel.sortOrder) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } case .failed: VStack(spacing: 0) { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistView.swift index f55511a47e..c417506372 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Watchlist/MarketWatchlistView.swift @@ -131,44 +131,48 @@ struct MarketWatchlistView: View { } @ViewBuilder private func list(marketInfos: [MarketInfo], signals: [String: TechnicalAdvice.Advice]) -> some View { - ThemeList( - marketInfos, - onMove: viewModel.sortBy == .manual ? { source, destination in - viewModel.move(source: source, destination: destination) - } : nil - ) { marketInfo in - let coin = marketInfo.fullCoin.coin - - ClickableRow(action: { - presentedFullCoin = marketInfo.fullCoin - }) { - itemContent( - imageUrl: URL(string: coin.imageUrl), - code: coin.code, - marketCap: marketInfo.marketCap, - price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, - rank: marketInfo.marketCapRank, - diff: marketInfo.priceChangeValue(timePeriod: viewModel.timePeriod), - signal: viewModel.showSignals ? signals[coin.uid] : nil - ) - } - .swipeActions { - Button(role: .destructive) { - viewModel.remove(coinUid: coin.uid) - } label: { - Image("star_off_24").renderingMode(.template) + ScrollViewReader { proxy in + ThemeList( + marketInfos, + invisibleTopView: true, + onMove: viewModel.sortBy == .manual ? { source, destination in + viewModel.move(source: source, destination: destination) + } : nil + ) { marketInfo in + let coin = marketInfo.fullCoin.coin + + ClickableRow(action: { + presentedFullCoin = marketInfo.fullCoin + }) { + itemContent( + imageUrl: URL(string: coin.imageUrl), + code: coin.code, + marketCap: marketInfo.marketCap, + price: marketInfo.price.flatMap { ValueFormatter.instance.formatFull(currency: viewModel.currency, value: $0) } ?? "n/a".localized, + rank: marketInfo.marketCapRank, + diff: marketInfo.priceChangeValue(timePeriod: viewModel.timePeriod), + signal: viewModel.showSignals ? signals[coin.uid] : nil + ) + } + .swipeActions { + Button(role: .destructive) { + viewModel.remove(coinUid: coin.uid) + } label: { + Image("star_off_24").renderingMode(.template) + } + .tint(.themeLucian) } - .tint(.themeLucian) } - } - .themeListStyle(.transparent) - .environment(\.editMode, $editMode) - .refreshable { - await viewModel.refresh() - } - .animation(.default, value: editMode) - .onChange(of: viewModel.sortBy) { _ in - editMode = .inactive + .environment(\.editMode, $editMode) + .refreshable { + await viewModel.refresh() + } + .animation(.default, value: editMode) + .onChange(of: viewModel.sortBy) { _ in + editMode = .inactive + withAnimation { proxy.scrollTo(themeListTopViewId) } + } + .onChange(of: viewModel.timePeriod) { _ in withAnimation { proxy.scrollTo(themeListTopViewId) } } } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift index 672eac2b44..e78b7a2092 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/SwiftUI/ThemeList.swift @@ -1,17 +1,29 @@ import SwiftUI import UIKit +let themeListTopViewId = "theme_list_top_view_id" + struct ThemeList: View { private let content: () -> Content private let bottomSpacing: CGFloat? + private let invisibleTopView: Bool - init(bottomSpacing: CGFloat? = nil, @ViewBuilder _ content: @escaping () -> Content) { + init(bottomSpacing: CGFloat? = nil, invisibleTopView: Bool = false, @ViewBuilder _ content: @escaping () -> Content) { self.bottomSpacing = bottomSpacing + self.invisibleTopView = invisibleTopView self.content = content } - init(_ items: [Item], bottomSpacing: CGFloat? = nil, onMove: ((IndexSet, Int) -> Void)? = nil, @ViewBuilder itemContent: @escaping (Item) -> ItemContent) where Content == ListForEach { + init( + _ items: [Item], + bottomSpacing: CGFloat? = nil, + invisibleTopView: Bool = false, + onMove: ((IndexSet, Int) -> Void)? = nil, + @ViewBuilder itemContent: @escaping (Item) -> ItemContent + ) where Content == ListForEach { self.bottomSpacing = bottomSpacing + self.invisibleTopView = invisibleTopView + content = { ListForEach(items, onMove: onMove, itemContent: itemContent) } @@ -19,6 +31,15 @@ struct ThemeList: View { var body: some View { List { + if invisibleTopView { + Color.clear + .id(themeListTopViewId) + .frame(height: 0) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + } + content() if let bottomSpacing { @@ -29,6 +50,7 @@ struct ThemeList: View { .listRowSeparator(.hidden) } } + .environment(\.defaultMinListRowHeight, 0) .listStyle(.plain) .themeListStyle(.transparent) }