From 00f556df4fc4a6bcaa9722c47a73ab5e367f0ed6 Mon Sep 17 00:00:00 2001 From: Vladyslav shepitko Date: Mon, 9 Aug 2021 09:46:01 +0300 Subject: [PATCH] [Samoa] Improve Add/Hide Tokens screen #2994 --- AlphaWallet.xcodeproj/project.pbxproj | 16 ++ .../Contents.json | 21 +++ .../iconsSystemExpandMore.pdf | Bin 0 -> 3849 bytes .../FilterTokensCoordinator.swift | 18 +++ AlphaWallet/Core/Types/SortTokensParam.swift | 27 ++++ .../Core/ViewModels/DropDownViewModel.swift | 48 ++++++ AlphaWallet/Core/Views/DropDownView.swift | 127 ++++++++++++++++ .../Localization/en.lproj/Localizable.strings | 6 + .../Localization/es.lproj/Localizable.strings | 6 + .../Localization/ja.lproj/Localizable.strings | 6 + .../Localization/ko.lproj/Localizable.strings | 6 + .../zh-Hans.lproj/Localizable.strings | 6 + AlphaWallet/Style/AppStyle.swift | 1 + .../AddHideTokensViewController.swift | 137 ++++++++++-------- .../TokensViewController.swift | 2 +- .../ViewModels/AddHideTokensViewModel.swift | 64 ++++++-- .../Views/EmptyFilteringResultView.swift | 81 +++++++++++ ...ViewControllerTableViewSectionHeader.swift | 58 +++++++- AlphaWallet/UI/Button.swift | 30 ++-- AlphaWallet/UI/ButtonsBar.swift | 2 +- 20 files changed, 583 insertions(+), 79 deletions(-) create mode 100644 AlphaWallet/Assets.xcassets/iconsSystemExpandMore.imageset/Contents.json create mode 100644 AlphaWallet/Assets.xcassets/iconsSystemExpandMore.imageset/iconsSystemExpandMore.pdf create mode 100644 AlphaWallet/Core/Types/SortTokensParam.swift create mode 100644 AlphaWallet/Core/ViewModels/DropDownViewModel.swift create mode 100644 AlphaWallet/Core/Views/DropDownView.swift create mode 100644 AlphaWallet/Tokens/Views/EmptyFilteringResultView.swift diff --git a/AlphaWallet.xcodeproj/project.pbxproj b/AlphaWallet.xcodeproj/project.pbxproj index 74986ea11..21905975d 100644 --- a/AlphaWallet.xcodeproj/project.pbxproj +++ b/AlphaWallet.xcodeproj/project.pbxproj @@ -870,6 +870,10 @@ 87ED8FAB2488E4610005C69B /* SendViewSectionHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87ED8FAA2488E4610005C69B /* SendViewSectionHeaderViewModel.swift */; }; 87ED8FAD2488EA610005C69B /* SupportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87ED8FAC2488EA610005C69B /* SupportViewController.swift */; }; 87ED8FAF2488EA9C0005C69B /* SupportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87ED8FAE2488EA9C0005C69B /* SupportViewModel.swift */; }; + 87F4D41A26C26C0700EFB9BC /* DropDownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F4D41926C26C0700EFB9BC /* DropDownView.swift */; }; + 87F4D41C26C26C2000EFB9BC /* DropDownViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F4D41B26C26C2000EFB9BC /* DropDownViewModel.swift */; }; + 87F4D41E26C26C3A00EFB9BC /* SortTokensParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F4D41D26C26C3A00EFB9BC /* SortTokensParam.swift */; }; + 87F4D42026C26C6E00EFB9BC /* EmptyFilteringResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F4D41F26C26C6E00EFB9BC /* EmptyFilteringResultView.swift */; }; 87F9972824E155280092D262 /* SeedPhraseBackupIntroductionViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87F9972724E155280092D262 /* SeedPhraseBackupIntroductionViewControllerTests.swift */; }; 87FBAE0124A1EE67005EF293 /* AddressOrEnsNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87FBAE0024A1EE67005EF293 /* AddressOrEnsNameLabel.swift */; }; 87FF2E422567F0A3002350EB /* BlockieGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87FF2E412567F0A3002350EB /* BlockieGenerator.swift */; }; @@ -1823,6 +1827,10 @@ 87ED8FAA2488E4610005C69B /* SendViewSectionHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendViewSectionHeaderViewModel.swift; sourceTree = ""; }; 87ED8FAC2488EA610005C69B /* SupportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportViewController.swift; sourceTree = ""; }; 87ED8FAE2488EA9C0005C69B /* SupportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportViewModel.swift; sourceTree = ""; }; + 87F4D41926C26C0700EFB9BC /* DropDownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropDownView.swift; sourceTree = ""; }; + 87F4D41B26C26C2000EFB9BC /* DropDownViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropDownViewModel.swift; sourceTree = ""; }; + 87F4D41D26C26C3A00EFB9BC /* SortTokensParam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortTokensParam.swift; sourceTree = ""; }; + 87F4D41F26C26C6E00EFB9BC /* EmptyFilteringResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyFilteringResultView.swift; sourceTree = ""; }; 87F9972724E155280092D262 /* SeedPhraseBackupIntroductionViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedPhraseBackupIntroductionViewControllerTests.swift; sourceTree = ""; }; 87FBAE0024A1EE67005EF293 /* AddressOrEnsNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressOrEnsNameLabel.swift; sourceTree = ""; }; 87FF2E412567F0A3002350EB /* BlockieGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockieGenerator.swift; sourceTree = ""; }; @@ -2226,6 +2234,7 @@ BBF4F9B62029D0B2009E04C0 /* GasViewModel.swift */, 2931120F1FC4ADCB00966EEA /* InCoordinatorViewModel.swift */, 5E7C7E50E9184C7F0FE3966C /* PromptViewModel.swift */, + 87F4D41B26C26C2000EFB9BC /* DropDownViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -2503,6 +2512,7 @@ 87D1757724ADAEEB002130D2 /* BlockchainTagLabel.swift */, 87620027266E14A80059B05A /* PopularTokenViewCell.swift */, 87620029266E14C10059B05A /* WalletTokenViewCell.swift */, + 87F4D41F26C26C6E00EFB9BC /* EmptyFilteringResultView.swift */, ); path = Views; sourceTree = ""; @@ -2968,6 +2978,7 @@ 87D457F426677CBE00BA1442 /* ERC20BalanceViewModel.swift */, 87D457F626677CD300BA1442 /* NativecryptoBalanceViewModel.swift */, 87C65F542661289400919819 /* Atomic.swift */, + 87F4D41D26C26C3A00EFB9BC /* SortTokensParam.swift */, ); path = Types; sourceTree = ""; @@ -3419,6 +3430,7 @@ 87FBAE0024A1EE67005EF293 /* AddressOrEnsNameLabel.swift */, 8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */, 874AF0822603405F00D613A5 /* LoadingIndicatorView.swift */, + 87F4D41926C26C0700EFB9BC /* DropDownView.swift */, ); path = Views; sourceTree = ""; @@ -5015,6 +5027,7 @@ 29FC0CB81F8299510036089F /* Coordinator.swift in Sources */, 29282B531F7630970067F88D /* TokenUpdate.swift in Sources */, 29F1C85D2003698A003780D8 /* WellDoneViewController.swift in Sources */, + 87F4D41C26C26C2000EFB9BC /* DropDownViewModel.swift in Sources */, 29E9CFCF1FE7347200017744 /* ERCToken.swift in Sources */, 5F4D80AA26A3F9C500BB1135 /* UIDevice.swift in Sources */, 29C9F5FB1F720C050025C494 /* FloatLabelTextField.swift in Sources */, @@ -5252,6 +5265,7 @@ 87374A4325DFAF7800267160 /* HoneySwap.swift in Sources */, 5E7C75C99B9F595F26EDC405 /* LockPasscodeViewController.swift in Sources */, 5E7C710331196CD591B51785 /* LockCreatePasscodeViewController.swift in Sources */, + 87F4D41E26C26C3A00EFB9BC /* SortTokensParam.swift in Sources */, 5E7C7499A8D6814F7950DA70 /* LockCreatePasscodeCoordinator.swift in Sources */, 87584B4425EFAFEC0070063B /* BuyTokenService.swift in Sources */, 5E7C71F8050CCF990539B293 /* LockView.swift in Sources */, @@ -5664,6 +5678,7 @@ 5E7C7D0A8197BF7725619D87 /* TransactionRowCellViewModel.swift in Sources */, 5E7C7BDE0060F5C350AFD34B /* ActivityViewController.swift in Sources */, 5E7C76194F5934264E5BABC8 /* ActivityViewModel.swift in Sources */, + 87F4D41A26C26C0700EFB9BC /* DropDownView.swift in Sources */, 5E7C72337A4230E78009B7E5 /* TransactionDetailsViewModel.swift in Sources */, 5E7C72EF748338DDBABB53F2 /* GetBlockTimestampCoordinator.swift in Sources */, 5E7C7416B5A023776E04A21D /* Features.swift in Sources */, @@ -5697,6 +5712,7 @@ 5E7C73FB906BF267F28B0205 /* ActivityRowType.swift in Sources */, 5E7C7AAA6892660CDBD84551 /* TransactionRow.swift in Sources */, 5E7C7594F29C8C9FE7BE0BEE /* RemoteLogger.swift in Sources */, + 87F4D42026C26C6E00EFB9BC /* EmptyFilteringResultView.swift in Sources */, 5E7C7EEB905CA4DEF2BD9608 /* ReplaceTransactionCoordinator.swift in Sources */, 5E7C7B2C388D0B6E8AE1EED6 /* TransactionConfirmationRowDescriptionView.swift in Sources */, 5E7C7068825D19D22329AC7E /* SendTransactionErrorViewController.swift in Sources */, diff --git a/AlphaWallet/Assets.xcassets/iconsSystemExpandMore.imageset/Contents.json b/AlphaWallet/Assets.xcassets/iconsSystemExpandMore.imageset/Contents.json new file mode 100644 index 000000000..56c52aac4 --- /dev/null +++ b/AlphaWallet/Assets.xcassets/iconsSystemExpandMore.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "iconsSystemExpandMore.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/AlphaWallet/Assets.xcassets/iconsSystemExpandMore.imageset/iconsSystemExpandMore.pdf b/AlphaWallet/Assets.xcassets/iconsSystemExpandMore.imageset/iconsSystemExpandMore.pdf new file mode 100644 index 0000000000000000000000000000000000000000..82d2dfd2860c61bb95c8139e928052c36ec51585 GIT binary patch literal 3849 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 literal 0 HcmV?d00001 diff --git a/AlphaWallet/Core/Coordinators/FilterTokensCoordinator.swift b/AlphaWallet/Core/Coordinators/FilterTokensCoordinator.swift index ad1cf1c86..f9c6f064a 100644 --- a/AlphaWallet/Core/Coordinators/FilterTokensCoordinator.swift +++ b/AlphaWallet/Core/Coordinators/FilterTokensCoordinator.swift @@ -117,4 +117,22 @@ class FilterTokensCoordinator { return result } + + func sortDisplayedTokens(tokens: [TokenObject], sortTokensParam: SortTokensParam) -> [TokenObject] { + let result = tokens.filter { + $0.shouldDisplay + }.sorted(by: { + switch sortTokensParam { + case .name: + return $0.name < $1.name + case .value: + return $0.value < $1.value + case .mostUsed: + // NOTE: not implemented yet + return false + } + }) + + return result + } } diff --git a/AlphaWallet/Core/Types/SortTokensParam.swift b/AlphaWallet/Core/Types/SortTokensParam.swift new file mode 100644 index 000000000..1e5a67cce --- /dev/null +++ b/AlphaWallet/Core/Types/SortTokensParam.swift @@ -0,0 +1,27 @@ +// +// SortTokensParam.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 10.08.2021. +// + +import UIKit + +enum SortTokensParam: Int, CaseIterable, DropDownItemType { + + var title: String { + switch self { + case .name: return R.string.localizable.sortTokensParamName() + case .value: return R.string.localizable.sortTokensParamValue() + case .mostUsed: return R.string.localizable.sortTokensParamMostUsed() + } + } + + case name + case value + case mostUsed + + static var allCases: [SortTokensParam] { + return [.name, .value] + } +} diff --git a/AlphaWallet/Core/ViewModels/DropDownViewModel.swift b/AlphaWallet/Core/ViewModels/DropDownViewModel.swift new file mode 100644 index 000000000..8492ab6b3 --- /dev/null +++ b/AlphaWallet/Core/ViewModels/DropDownViewModel.swift @@ -0,0 +1,48 @@ +// +// DropDownViewModel.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 10.08.2021. +// + +import UIKit + + +protocol DropDownItemType: RawRepresentable, Equatable { + var title: String { get } +} + +struct DropDownViewModel { + let selectionItems: [T] + var selected: SegmentedControl.Selection + var placeholder: String = R.string.localizable.sortTokensSortBy("-") + + func placeholder(for selection: SegmentedControl.Selection) -> String { + switch selection { + case .unselected: + return placeholder + case .selected(let idx): + return R.string.localizable.sortTokensSortBy(selectionItems[Int(idx)].title) + } + } + + init(selectionItems: [T], selected: T) { + self.selectionItems = selectionItems + self.selected = DropDownViewModel.elementSelection(of: selected, in: selectionItems) + } + + func attributedString(item: T) -> NSAttributedString { + return NSAttributedString(string: item.title, attributes: [ + .font: Fonts.regular(size: 23), + .foregroundColor: Colors.sortByTextColor + ]) + } + + static func elementSelection(of selected: T, in selectionItems: [T]) -> SegmentedControl.Selection { + guard let index = selectionItems.firstIndex(where: { $0 == selected }) else { + return .unselected + } + + return .selected(UInt(index)) + } +} diff --git a/AlphaWallet/Core/Views/DropDownView.swift b/AlphaWallet/Core/Views/DropDownView.swift new file mode 100644 index 000000000..11679fa56 --- /dev/null +++ b/AlphaWallet/Core/Views/DropDownView.swift @@ -0,0 +1,127 @@ +// +// DropDownView.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 10.08.2021. +// + +import UIKit + +protocol DropDownViewDelegate: class { + func filterDropDownViewDidChange(selection: SegmentedControl.Selection) +} + +final class DropDownView: UIView, ReusableTableHeaderViewType, UIPickerViewDelegate, UIPickerViewDataSource { + + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return viewModel.selectionItems.count + } + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + return viewModel.selectionItems[row].title + } + + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + return viewModel.attributedString(item: viewModel.selectionItems[row]) + } + + private var selected: SegmentedControl.Selection + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + selected = .selected(UInt(row)) + } + + private var viewModel: DropDownViewModel + weak var delegate: DropDownViewDelegate? + + private lazy var hiddenTextField: UITextField = { + let textField = UITextField() + textField.translatesAutoresizingMaskIntoConstraints = false + textField.inputAccessoryView = UIToolbar.doneToolbarButton(#selector(doneSelected), self) + textField.inputView = pickerView + textField.isHidden = true + + return textField + }() + + private lazy var pickerView: UIPickerView = { + let pickerView = UIPickerView(frame: CGRect(x: 0, y: 0, width: bounds.size.width, height: 200)) + pickerView.translatesAutoresizingMaskIntoConstraints = false + pickerView.delegate = self + pickerView.dataSource = self + + return pickerView + }() + + private lazy var selectionButton: Button = { + let button = Button(size: .normal, style: .special) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(selectionButtonSelected), for: .touchUpInside) + button.setImage(R.image.iconsSystemExpandMore(), for: .normal) + button.imageView?.contentMode = .scaleAspectFit + button.semanticContentAttribute = .forceRightToLeft + + return button + }() + + init(viewModel: DropDownViewModel) { + self.viewModel = viewModel + self.selected = viewModel.selected + super.init(frame: .zero) + + translatesAutoresizingMaskIntoConstraints = false + addSubview(hiddenTextField) + addSubview(selectionButton) + + NSLayoutConstraint.activate([ + selectionButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), + selectionButton.topAnchor.constraint(equalTo: topAnchor, constant: 16), + selectionButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16), + ]) + + configure(viewModel: viewModel) + } + + required init?(coder: NSCoder) { + return nil + } + + func configure(viewModel: DropDownViewModel) { + self.viewModel = viewModel + self.selected = viewModel.selected + configure(selection: viewModel.selected) + } + + @objc private func selectionButtonSelected(_ sender: UIButton) { + hiddenTextField.becomeFirstResponder() + } + + @objc private func doneSelected(_ sender: UITextField) { + hiddenTextField.endEditing(true) + + viewModel.selected = selected + configure(selection: viewModel.selected) + + delegate?.filterDropDownViewDidChange(selection: viewModel.selected) + } + + private func configure(selection: SegmentedControl.Selection) { + let placeholder = viewModel.placeholder(for: selection) + selectionButton.setTitle(placeholder, for: .normal) + selectionButton.semanticContentAttribute = .forceRightToLeft + } + + func value(from selection: SegmentedControl.Selection) -> T? { + switch selection { + case .unselected: + return nil + case .selected(let index): + guard viewModel.selectionItems.indices.contains(Int(index)) else { return nil } + return viewModel.selectionItems[Int(index)] + } + } +} + diff --git a/AlphaWallet/Localization/en.lproj/Localizable.strings b/AlphaWallet/Localization/en.lproj/Localizable.strings index 21abb108d..563bbd803 100644 --- a/AlphaWallet/Localization/en.lproj/Localizable.strings +++ b/AlphaWallet/Localization/en.lproj/Localizable.strings @@ -661,3 +661,9 @@ You can check the latest gas price on gasnow.org"; "token.info.field.stats.max_supply" = "Max Supply"; "token.info.field.perfomance.year.low" = "1 Year Low"; "token.info.field.perfomance.year.high" = "1 Year High"; +"addCustomToken.title" = "Add Custom Token"; +"seachToken.noresults.title" = "No results for token you are searching for"; +"sortTokens.param.name" = "Name"; +"sortTokens.param.value" = "Value"; +"sortTokens.param.mostUsed" = "Most Used"; +"sortTokens.sortBy" = "Sort: By %@"; diff --git a/AlphaWallet/Localization/es.lproj/Localizable.strings b/AlphaWallet/Localization/es.lproj/Localizable.strings index 7ef8295e7..afb3453b1 100644 --- a/AlphaWallet/Localization/es.lproj/Localizable.strings +++ b/AlphaWallet/Localization/es.lproj/Localizable.strings @@ -661,3 +661,9 @@ You can check the latest gas price on gasnow.org"; "token.info.field.stats.max_supply" = "Max Supply"; "token.info.field.perfomance.year.low" = "1 Year Low"; "token.info.field.perfomance.year.high" = "1 Year High"; +"addCustomToken.title" = "Add Custom Token"; +"seachToken.noresults.title" = "No results for token you are searching for"; +"sortTokens.param.name" = "Name"; +"sortTokens.param.value" = "Value"; +"sortTokens.param.mostUsed" = "Most Used"; +"sortTokens.sortBy" = "Sort: By %@"; diff --git a/AlphaWallet/Localization/ja.lproj/Localizable.strings b/AlphaWallet/Localization/ja.lproj/Localizable.strings index 9a24a1ae8..4bcf65dab 100644 --- a/AlphaWallet/Localization/ja.lproj/Localizable.strings +++ b/AlphaWallet/Localization/ja.lproj/Localizable.strings @@ -661,3 +661,9 @@ You can check the latest gas price on gasnow.org"; "token.info.field.stats.max_supply" = "Max Supply"; "token.info.field.perfomance.year.low" = "1 Year Low"; "token.info.field.perfomance.year.high" = "1 Year High"; +"addCustomToken.title" = "Add Custom Token"; +"seachToken.noresults.title" = "No results for token you are searching for"; +"sortTokens.param.name" = "Name"; +"sortTokens.param.value" = "Value"; +"sortTokens.param.mostUsed" = "Most Used"; +"sortTokens.sortBy" = "Sort: By %@"; diff --git a/AlphaWallet/Localization/ko.lproj/Localizable.strings b/AlphaWallet/Localization/ko.lproj/Localizable.strings index d0f111a77..dafc17b29 100644 --- a/AlphaWallet/Localization/ko.lproj/Localizable.strings +++ b/AlphaWallet/Localization/ko.lproj/Localizable.strings @@ -661,3 +661,9 @@ You can check the latest gas price on gasnow.org"; "token.info.field.stats.max_supply" = "Max Supply"; "token.info.field.perfomance.year.low" = "1 Year Low"; "token.info.field.perfomance.year.high" = "1 Year High"; +"addCustomToken.title" = "Add Custom Token"; +"seachToken.noresults.title" = "No results for token you are searching for"; +"sortTokens.param.name" = "Name"; +"sortTokens.param.value" = "Value"; +"sortTokens.param.mostUsed" = "Most Used"; +"sortTokens.sortBy" = "Sort: By %@"; diff --git a/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings b/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings index 91cdac048..ae19261bf 100644 --- a/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings +++ b/AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings @@ -661,3 +661,9 @@ You can check the latest gas price on gasnow.org"; "token.info.field.stats.max_supply" = "Max Supply"; "token.info.field.perfomance.year.low" = "1 Year Low"; "token.info.field.perfomance.year.high" = "1 Year High"; +"addCustomToken.title" = "Add Custom Token"; +"seachToken.noresults.title" = "No results for token you are searching for"; +"sortTokens.param.name" = "Name"; +"sortTokens.param.value" = "Value"; +"sortTokens.param.mostUsed" = "Most Used"; +"sortTokens.sortBy" = "Sort: By %@"; diff --git a/AlphaWallet/Style/AppStyle.swift b/AlphaWallet/Style/AppStyle.swift index 44916e2b6..d80db74c2 100644 --- a/AlphaWallet/Style/AppStyle.swift +++ b/AlphaWallet/Style/AppStyle.swift @@ -70,6 +70,7 @@ struct Colors { static let settingsSubtitle = UIColor(red: 141, green: 141, blue: 141) static let qrCodeRectBorders = UIColor(red: 216, green: 216, blue: 216) static let loadingIndicatorBorder = UIColor(red: 237, green: 237, blue: 237) + static let sortByTextColor = UIColor(red: 51, green: 51, blue: 51) } struct StyleLayout { diff --git a/AlphaWallet/Tokens/ViewControllers/AddHideTokensViewController.swift b/AlphaWallet/Tokens/ViewControllers/AddHideTokensViewController.swift index f55d4c455..3ae726e21 100644 --- a/AlphaWallet/Tokens/ViewControllers/AddHideTokensViewController.swift +++ b/AlphaWallet/Tokens/ViewControllers/AddHideTokensViewController.swift @@ -5,7 +5,7 @@ import StatefulViewController import PromiseKit protocol AddHideTokensViewControllerDelegate: AnyObject { - func didPressAddToken( in viewController: UIViewController) + func didPressAddToken(in viewController: UIViewController) func didMark(token: TokenObject, in viewController: UIViewController, isHidden: Bool) func didChangeOrder(tokens: [TokenObject], in viewController: UIViewController) func didClose(viewController: AddHideTokensViewController) @@ -21,6 +21,7 @@ class AddHideTokensViewController: UIViewController { tableView.register(WalletTokenViewCell.self) tableView.register(PopularTokenViewCell.self) tableView.registerHeaderFooterView(AddHideTokenSectionHeaderView.self) + tableView.registerHeaderFooterView(TokensViewController.GeneralTableViewSectionHeader>.self) tableView.isEditing = true tableView.estimatedRowHeight = 100 tableView.dataSource = self @@ -31,98 +32,82 @@ class AddHideTokensViewController: UIViewController { tableView.contentOffset = .zero tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.size.width, height: 0.01)) tableView.translatesAutoresizingMaskIntoConstraints = false + return tableView }() private let refreshControl = UIRefreshControl() - private var prefersLargeTitles: Bool? - private let notificationCenter = NotificationCenter.default + + private lazy var tokenFilterView: DropDownView = { + let view = DropDownView(viewModel: .init(selectionItems: SortTokensParam.allCases, selected: viewModel.sortTokensParam)) + view.delegate = self + + return view + }() + private var bottomConstraint: NSLayoutConstraint! + private lazy var keyboardChecker = KeyboardChecker(self, resetHeightDefaultValue: 0, ignoreBottomSafeArea: true) + weak var delegate: AddHideTokensViewControllerDelegate? init(viewModel: AddHideTokensViewModel, assetDefinitionStore: AssetDefinitionStore) { self.assetDefinitionStore = assetDefinitionStore self.viewModel = viewModel searchController = UISearchController(searchResultsController: nil) - super.init(nibName: nil, bundle: nil) hidesBottomBarWhenPushed = true + searchController.delegate = self + + let t = R.string.localizable.seachTokenNoresultsTitle() + emptyView = EmptyFilteringResultView(title: t, onRetry: { [weak self] in + guard let strongSelf = self, let delegate = strongSelf.delegate else { return } + + delegate.didPressAddToken(in: strongSelf) + }) + + view.addSubview(tableView) + + bottomConstraint = tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + keyboardChecker.constraint = bottomConstraint + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + bottomConstraint + ]) } required init?(coder: NSCoder) { return nil } - override func loadView() { - view = tableView - } - override func viewDidLoad() { super.viewDidLoad() configure(viewModel: viewModel) setupFilteringWithKeyword() + navigationItem.largeTitleDisplayMode = .never navigationItem.rightBarButtonItem = UIBarButtonItem.addButton(self, selector: #selector(addToken)) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - notificationCenter.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) - notificationCenter.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) - - prefersLargeTitles = navigationController?.navigationBar.prefersLargeTitles - navigationController?.navigationBar.prefersLargeTitles = false - + keyboardChecker.viewWillAppear() reload() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - - notificationCenter.removeObserver(self) + keyboardChecker.viewWillDisappear() if isMovingFromParent || isBeingDismissed { - if let prefersLargeTitles = prefersLargeTitles { - //This unfortunately breaks the smooth animation if we pop back and show the large title - navigationController?.navigationBar.prefersLargeTitles = prefersLargeTitles - } delegate?.didClose(viewController: self) return } } - @objc private func keyboardWillShow(_ notification: Notification) { - guard let change = notification.keyboardInfo else { - return - } - - let bottom = change.endFrame.height - UIApplication.shared.bottomSafeAreaHeight - - UIView.setAnimationCurve(change.curve) - UIView.animate(withDuration: change.duration, animations: { - self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottom, right: 0) - self.tableView.scrollIndicatorInsets = self.tableView.contentInset - }, completion: { _ in - - }) - } - - @objc private func keyboardWillHide(_ notification: Notification) { - guard let change = notification.keyboardInfo else { - return - } - - UIView.setAnimationCurve(change.curve) - UIView.animate(withDuration: change.duration, animations: { - self.tableView.contentInset = .zero - self.tableView.scrollIndicatorInsets = self.tableView.contentInset - }, completion: { _ in - - }) - } - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() configureSearchBarOnce() } @@ -134,10 +119,14 @@ class AddHideTokensViewController: UIViewController { title = viewModel.title tableView.backgroundColor = viewModel.backgroundColor view.backgroundColor = viewModel.backgroundColor + + tokenFilterView.configure(viewModel: .init(selectionItems: SortTokensParam.allCases, selected: viewModel.sortTokensParam)) } private func reload() { + startLoading(animated: false) tableView.reloadData() + endLoading(animated: false) } func add(token: TokenObject) { @@ -157,7 +146,7 @@ class AddHideTokensViewController: UIViewController { extension AddHideTokensViewController: StatefulViewController { //Always return true, otherwise users will be stuck in the assets sub-tab when they have no assets func hasContent() -> Bool { - true + return !viewModel.sections.isEmpty } } @@ -301,25 +290,59 @@ extension AddHideTokensViewController: UITableViewDelegate { } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let view: AddHideTokenSectionHeaderView = tableView.dequeueReusableHeaderFooterView() - view.configure(viewModel: .init(text: viewModel.titleForSection(section))) - - return view + switch viewModel.sections[section] { + case .sortingFilters: + let header: TokensViewController.GeneralTableViewSectionHeader> = tableView.dequeueReusableHeaderFooterView() + header.useSeparatorLine = true + header.subview = tokenFilterView + + return header + case .availableNewTokens, .popularTokens, .hiddenTokens, .displayedTokens: + let view: AddHideTokenSectionHeaderView = tableView.dequeueReusableHeaderFooterView() + view.configure(viewModel: .init(text: viewModel.titleForSection(section))) + + return view + } } func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - 65 + switch viewModel.sections[section] { + case .sortingFilters: + return 60 + case .availableNewTokens, .popularTokens, .hiddenTokens, .displayedTokens: + return 65 + } } //Hide the footer func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { .leastNormalMagnitude } + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { nil } } +extension AddHideTokensViewController: DropDownViewDelegate { + func filterDropDownViewDidChange(selection: SegmentedControl.Selection) { + guard let filterParam = tokenFilterView.value(from: selection) else { return } + + viewModel.sortTokensParam = filterParam + reload() + } +} + +extension AddHideTokensViewController: UISearchControllerDelegate { + func willPresentSearchController(_ searchController: UISearchController) { + viewModel.isSearchActive = true + } + + func willDismissSearchController(_ searchController: UISearchController) { + viewModel.isSearchActive = false + } +} + extension AddHideTokensViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { DispatchQueue.main.async { [weak self] in diff --git a/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift b/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift index 1aed09fc1..e2f0ac7ad 100644 --- a/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift +++ b/AlphaWallet/Tokens/ViewControllers/TokensViewController.swift @@ -461,7 +461,7 @@ extension TokensViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { switch sections[section] { case .walletSummary: - let header: GeneralTableViewSectionHeader = tableView.dequeueReusableHeaderFooterView() + let header: TokensViewController.GeneralTableViewSectionHeader = tableView.dequeueReusableHeaderFooterView() header.subview = walletSummaryView return header diff --git a/AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift b/AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift index 1ad20c94b..b6d596cc1 100644 --- a/AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift +++ b/AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift @@ -3,7 +3,8 @@ import UIKit import PromiseKit -private enum AddHideTokenSections: Int { +enum AddHideTokenSections: Int { + case sortingFilters case availableNewTokens case displayedTokens case hiddenTokens @@ -11,6 +12,8 @@ private enum AddHideTokenSections: Int { var description: String { switch self { + case .sortingFilters: + return String() case .availableNewTokens: return R.string.localizable.addHideTokensSectionNewTokens() case .displayedTokens: @@ -21,19 +24,27 @@ private enum AddHideTokenSections: Int { return R.string.localizable.addHideTokensSectionPopularTokens() } } + + static var enabledSectins: [AddHideTokenSections] { + [.sortingFilters, .displayedTokens, .hiddenTokens, .popularTokens] + } } //NOTE: Changed to class to prevent update all ViewModel copies and apply updates only in one place. class AddHideTokensViewModel { - private let sections: [AddHideTokenSections] = [.displayedTokens, .hiddenTokens, .popularTokens] + var sections: [AddHideTokenSections] = [.sortingFilters, .displayedTokens, .hiddenTokens, .popularTokens] private let filterTokensCoordinator: FilterTokensCoordinator private var tokens: [TokenObject] private var allPopularTokens: [PopularToken] = [] - private var displayedTokens: [TokenObject] = [] private var hiddenTokens: [TokenObject] = [] private var popularTokens: [PopularToken] = [] + var sortTokensParam: SortTokensParam = .name { + didSet { + filter(tokens: tokens) + } + } var searchText: String? { didSet { filter(tokens: tokens) @@ -81,6 +92,8 @@ class AddHideTokensViewModel { return 0 case .popularTokens: return popularTokens.count + case .sortingFilters: + return 0 } } @@ -88,7 +101,7 @@ class AddHideTokensViewModel { switch sections[indexPath.section] { case .displayedTokens: return true - case .availableNewTokens, .popularTokens, .hiddenTokens: + case .availableNewTokens, .popularTokens, .hiddenTokens, .sortingFilters: return false } } @@ -114,7 +127,7 @@ class AddHideTokensViewModel { func addDisplayed(indexPath: IndexPath) -> ShowHideOperationResult { switch sections[indexPath.section] { - case .displayedTokens, .availableNewTokens: + case .displayedTokens, .availableNewTokens, .sortingFilters: break case .hiddenTokens: let token = hiddenTokens.remove(at: indexPath.row) @@ -152,7 +165,7 @@ class AddHideTokensViewModel { if let sectionIndex = sections.index(of: .hiddenTokens) { return .value((token, IndexPath(row: 0, section: Int(sectionIndex)))) } - case .hiddenTokens, .availableNewTokens, .popularTokens: + case .hiddenTokens, .availableNewTokens, .popularTokens, .sortingFilters: break } @@ -165,6 +178,8 @@ class AddHideTokensViewModel { return .delete case .availableNewTokens, .popularTokens, .hiddenTokens: return .insert + case .sortingFilters: + return .none } } @@ -172,7 +187,7 @@ class AddHideTokensViewModel { switch sections[indexPath.section] { case .displayedTokens: return true - case .availableNewTokens, .popularTokens, .hiddenTokens: + case .availableNewTokens, .popularTokens, .hiddenTokens, .sortingFilters: return false } } @@ -187,6 +202,8 @@ class AddHideTokensViewModel { return nil case .popularTokens: return .popularToken(popularTokens[indexPath.row]) + case .sortingFilters: + return nil } } @@ -197,10 +214,11 @@ class AddHideTokensViewModel { displayedTokens.insert(token, at: to.row) return displayedTokens - case .hiddenTokens, .availableNewTokens, .popularTokens: + case .hiddenTokens, .availableNewTokens, .popularTokens, .sortingFilters: return nil } } + var isSearchActive: Bool = false private func filter(tokens: [TokenObject]) { displayedTokens.removeAll() @@ -215,7 +233,11 @@ class AddHideTokensViewModel { } } popularTokens = filterTokensCoordinator.filterTokens(tokens: allPopularTokens, walletTokens: tokens, filter: .keyword(searchText ?? "")) - displayedTokens = filterTokensCoordinator.sortDisplayedTokens(tokens: displayedTokens) + + displayedTokens = filterTokensCoordinator.sortDisplayedTokens(tokens: displayedTokens, sortTokensParam: sortTokensParam) + hiddenTokens = filterTokensCoordinator.sortDisplayedTokens(tokens: hiddenTokens, sortTokensParam: sortTokensParam) + + sections = AddHideTokensViewModel.functional.availableSectionsToDisplay(displayedTokens: displayedTokens, hiddenTokens: hiddenTokens, popularTokens: popularTokens, isSearchActive: isSearchActive) } private func fetchContractDataPromise(forServer server: RPCServer, address: AlphaWallet.Address) -> Promise { @@ -228,3 +250,27 @@ class AddHideTokensViewModel { private struct RetrieveSingleChainTokenCoordinator: Error { } } + +extension AddHideTokensViewModel { + class functional {} +} + +extension AddHideTokensViewModel.functional { + static func availableSectionsToDisplay(displayedTokens: [Any], hiddenTokens: [Any], popularTokens: [Any], isSearchActive: Bool) -> [AddHideTokenSections] { + if isSearchActive { + var sections: [AddHideTokenSections] = [] + if !displayedTokens.isEmpty { + sections.append(.displayedTokens) + } + if !hiddenTokens.isEmpty { + sections.append(.hiddenTokens) + } + if !popularTokens.isEmpty { + sections.append(.popularTokens) + } + return sections + } else { + return AddHideTokenSections.enabledSectins + } + } +} diff --git a/AlphaWallet/Tokens/Views/EmptyFilteringResultView.swift b/AlphaWallet/Tokens/Views/EmptyFilteringResultView.swift new file mode 100644 index 000000000..7e0d87811 --- /dev/null +++ b/AlphaWallet/Tokens/Views/EmptyFilteringResultView.swift @@ -0,0 +1,81 @@ +// +// EmptyFilteringResultView.swift +// AlphaWallet +// +// Created by Vladyslav Shepitko on 10.08.2021. +// + +import Foundation +import UIKit +import StatefulViewController + +class EmptyFilteringResultView: UIView { + private let titleLabel = UILabel() + private let imageView = UIImageView() + private let button = Button(size: .large, style: .green) + private let insets: UIEdgeInsets + var onRetry: (() -> Void)? = .none + private let viewModel = StateViewModel() + + init( + frame: CGRect = .zero, + title: String = R.string.localizable.empty(), + image: UIImage? = R.image.no_transactions_mascot(), + insets: UIEdgeInsets = .zero, + actionButtonTitle: String = R.string.localizable.addCustomTokenTitle(), + onRetry: (() -> Void)? = .none + ) { + self.insets = insets + self.onRetry = onRetry + super.init(frame: frame) + + backgroundColor = .white + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.text = title + titleLabel.font = viewModel.descriptionFont + titleLabel.textColor = viewModel.descriptionTextColor + + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = image + + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(actionButtonTitle, for: .normal) + button.addTarget(self, action: #selector(retry), for: .touchUpInside) + + let stackView = [ + imageView, + titleLabel, + ].asStackView(axis: .vertical, spacing: 30, alignment: .center) + stackView.translatesAutoresizingMaskIntoConstraints = false + + if onRetry != nil { + stackView.addArrangedSubview(button) + } + + addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.centerXAnchor.constraint(equalTo: centerXAnchor), + stackView.bottomAnchor.constraint(equalTo: centerYAnchor, constant: -30), + button.widthAnchor.constraint(equalToConstant: 230), + ]) + } + + @objc func retry() { + onRetry?() + } + + required init?(coder aDecoder: NSCoder) { + return nil + } +} + +extension EmptyFilteringResultView: StatefulPlaceholderView { + func placeholderViewInsets() -> UIEdgeInsets { + return insets + } +} + diff --git a/AlphaWallet/Tokens/Views/TokensViewControllerTableViewSectionHeader.swift b/AlphaWallet/Tokens/Views/TokensViewControllerTableViewSectionHeader.swift index deaf63825..62002a01d 100644 --- a/AlphaWallet/Tokens/Views/TokensViewControllerTableViewSectionHeader.swift +++ b/AlphaWallet/Tokens/Views/TokensViewControllerTableViewSectionHeader.swift @@ -45,22 +45,56 @@ extension TokensViewController { } return } + subview.backgroundColor = Colors.appWhite subview.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(subview) + contentView.addSubview(bottomSeparator) + contentView.addSubview(topSeparator) + NSLayoutConstraint.activate([ subview.anchorsConstraint(to: contentView), - ]) + ] + topSeparator.anchorSeparatorToTop(to: contentView) + bottomSeparator.anchorSeparatorToBottom(to: contentView)) + } + } + + private var bottomSeparator: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + private var topSeparator: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + var useSeparatorLine: Bool { + get { + !bottomSeparator.isHidden + } + set { + bottomSeparator.isHidden = !newValue + topSeparator.isHidden = !newValue } } override var reuseIdentifier: String? { - subview?.restorationIdentifier + T.reusableIdentifier } override init(reuseIdentifier: String?) { super.init(reuseIdentifier: reuseIdentifier) + contentView.backgroundColor = Colors.appWhite + bottomSeparator.isHidden = true + topSeparator.isHidden = true + + bottomSeparator.backgroundColor = GroupedTable.Color.cellSeparator + topSeparator.backgroundColor = GroupedTable.Color.cellSeparator } required init?(coder aDecoder: NSCoder) { @@ -68,3 +102,23 @@ extension TokensViewController { } } } + +extension UIView { + func anchorSeparatorToTop(to superView: UIView) -> [NSLayoutConstraint] { + return [ + centerXAnchor.constraint(equalTo: superView.centerXAnchor), + widthAnchor.constraint(equalTo: superView.widthAnchor), + heightAnchor.constraint(equalToConstant: GroupedTable.Metric.cellSeparatorHeight), + topAnchor.constraint(equalTo: superView.topAnchor) + ] + } + + func anchorSeparatorToBottom(to superView: UIView) -> [NSLayoutConstraint] { + return [ + centerXAnchor.constraint(equalTo: superView.centerXAnchor), + widthAnchor.constraint(equalTo: superView.widthAnchor), + heightAnchor.constraint(equalToConstant: GroupedTable.Metric.cellSeparatorHeight), + bottomAnchor.constraint(equalTo: superView.bottomAnchor) + ] + } +} diff --git a/AlphaWallet/UI/Button.swift b/AlphaWallet/UI/Button.swift index 7dbb60d17..f56fc1507 100644 --- a/AlphaWallet/UI/Button.swift +++ b/AlphaWallet/UI/Button.swift @@ -19,18 +19,22 @@ enum ButtonSize: Int { } } -enum ButtonStyle: Int { +enum ButtonStyle { case solid case squared case border case borderless case system + case special + case green var backgroundColor: UIColor { switch self { case .solid, .squared: return Colors.appTint case .border, .borderless: return .white case .system: return .clear + case .special: return R.color.concrete()! + case .green: return ButtonsBarViewModel.greenButton.buttonBackgroundColor } } @@ -40,6 +44,8 @@ enum ButtonStyle: Int { case .border: return Colors.appTint case .borderless: return .white case .system: return .clear + case .special: return R.color.concrete()! + case .green: return ButtonsBarViewModel.greenButton.buttonBackgroundColor } } @@ -47,6 +53,8 @@ enum ButtonStyle: Int { switch self { case .solid, .border: return 5 case .squared, .borderless, .system: return 0 + case .special: return 12 + case .green: return ButtonsBarViewModel.greenButton.buttonCornerRadius } } @@ -55,15 +63,17 @@ enum ButtonStyle: Int { case .solid, .squared, .border, - .borderless, .system: + .borderless, .system, .special: return Fonts.semibold(size: 16) + case .green: return ButtonsBarViewModel.greenButton.buttonFont } } var textColor: UIColor { switch self { case .solid, .squared: return Colors.appWhite - case .border, .borderless, .system: return Colors.appTint + case .border, .borderless, .system, .special: return Colors.appTint + case .green: return ButtonsBarViewModel.greenButton.buttonTitleColor } } @@ -71,21 +81,24 @@ enum ButtonStyle: Int { switch self { case .solid, .squared: return UIColor(white: 1, alpha: 0.8) case .border: return Colors.appWhite - case .borderless, .system: return Colors.appTint + case .borderless, .system, .special: return Colors.appTint + case .green: return ButtonsBarViewModel.greenButton.buttonBackgroundColor } } var borderColor: UIColor { switch self { case .solid, .squared, .border: return GroupedTable.Color.background - case .borderless, .system: return .clear + case .borderless, .system, .special: return .clear + case .green: return ButtonsBarViewModel.greenButton.buttonBorderColor } } var borderWidth: CGFloat { switch self { - case .solid, .squared, .borderless, .system: return 0 + case .solid, .squared, .borderless, .system, .special: return 0 case .border: return 1 + case .green: return ButtonsBarViewModel.greenButton.buttonBorderWidth } } } @@ -98,13 +111,13 @@ class Button: UIButton { } required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + return nil } func apply(size: ButtonSize, style: ButtonStyle) { NSLayoutConstraint.activate([ heightAnchor.constraint(equalToConstant: size.height), - ]) + ]) backgroundColor = style.backgroundColor layer.cornerRadius = style.cornerRadius @@ -119,5 +132,4 @@ class Button: UIButton { setBackgroundColor(style.backgroundColorHighlighted, forState: .selected) contentEdgeInsets = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10) } - } diff --git a/AlphaWallet/UI/ButtonsBar.swift b/AlphaWallet/UI/ButtonsBar.swift index 0c7d863bb..6cbb2d88f 100644 --- a/AlphaWallet/UI/ButtonsBar.swift +++ b/AlphaWallet/UI/ButtonsBar.swift @@ -350,7 +350,7 @@ class ButtonsBar: UIView { } } -private struct ButtonsBarViewModel { +struct ButtonsBarViewModel { static let greenButton = ButtonsBarViewModel( buttonBackgroundColor: Colors.appActionButtonGreen,