[Samoa] Improve Add/Hide Tokens screen #2994

pull/3041/head
Vladyslav shepitko 3 years ago
parent 86555ad46d
commit 00f556df4f
  1. 16
      AlphaWallet.xcodeproj/project.pbxproj
  2. 21
      AlphaWallet/Assets.xcassets/iconsSystemExpandMore.imageset/Contents.json
  3. BIN
      AlphaWallet/Assets.xcassets/iconsSystemExpandMore.imageset/iconsSystemExpandMore.pdf
  4. 18
      AlphaWallet/Core/Coordinators/FilterTokensCoordinator.swift
  5. 27
      AlphaWallet/Core/Types/SortTokensParam.swift
  6. 48
      AlphaWallet/Core/ViewModels/DropDownViewModel.swift
  7. 127
      AlphaWallet/Core/Views/DropDownView.swift
  8. 6
      AlphaWallet/Localization/en.lproj/Localizable.strings
  9. 6
      AlphaWallet/Localization/es.lproj/Localizable.strings
  10. 6
      AlphaWallet/Localization/ja.lproj/Localizable.strings
  11. 6
      AlphaWallet/Localization/ko.lproj/Localizable.strings
  12. 6
      AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings
  13. 1
      AlphaWallet/Style/AppStyle.swift
  14. 137
      AlphaWallet/Tokens/ViewControllers/AddHideTokensViewController.swift
  15. 2
      AlphaWallet/Tokens/ViewControllers/TokensViewController.swift
  16. 64
      AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift
  17. 81
      AlphaWallet/Tokens/Views/EmptyFilteringResultView.swift
  18. 58
      AlphaWallet/Tokens/Views/TokensViewControllerTableViewSectionHeader.swift
  19. 30
      AlphaWallet/UI/Button.swift
  20. 2
      AlphaWallet/UI/ButtonsBar.swift

@ -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 = "<group>"; };
87ED8FAC2488EA610005C69B /* SupportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportViewController.swift; sourceTree = "<group>"; };
87ED8FAE2488EA9C0005C69B /* SupportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportViewModel.swift; sourceTree = "<group>"; };
87F4D41926C26C0700EFB9BC /* DropDownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropDownView.swift; sourceTree = "<group>"; };
87F4D41B26C26C2000EFB9BC /* DropDownViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropDownViewModel.swift; sourceTree = "<group>"; };
87F4D41D26C26C3A00EFB9BC /* SortTokensParam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortTokensParam.swift; sourceTree = "<group>"; };
87F4D41F26C26C6E00EFB9BC /* EmptyFilteringResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyFilteringResultView.swift; sourceTree = "<group>"; };
87F9972724E155280092D262 /* SeedPhraseBackupIntroductionViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedPhraseBackupIntroductionViewControllerTests.swift; sourceTree = "<group>"; };
87FBAE0024A1EE67005EF293 /* AddressOrEnsNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressOrEnsNameLabel.swift; sourceTree = "<group>"; };
87FF2E412567F0A3002350EB /* BlockieGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockieGenerator.swift; sourceTree = "<group>"; };
@ -2226,6 +2234,7 @@
BBF4F9B62029D0B2009E04C0 /* GasViewModel.swift */,
2931120F1FC4ADCB00966EEA /* InCoordinatorViewModel.swift */,
5E7C7E50E9184C7F0FE3966C /* PromptViewModel.swift */,
87F4D41B26C26C2000EFB9BC /* DropDownViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
@ -2503,6 +2512,7 @@
87D1757724ADAEEB002130D2 /* BlockchainTagLabel.swift */,
87620027266E14A80059B05A /* PopularTokenViewCell.swift */,
87620029266E14C10059B05A /* WalletTokenViewCell.swift */,
87F4D41F26C26C6E00EFB9BC /* EmptyFilteringResultView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -2968,6 +2978,7 @@
87D457F426677CBE00BA1442 /* ERC20BalanceViewModel.swift */,
87D457F626677CD300BA1442 /* NativecryptoBalanceViewModel.swift */,
87C65F542661289400919819 /* Atomic.swift */,
87F4D41D26C26C3A00EFB9BC /* SortTokensParam.swift */,
);
path = Types;
sourceTree = "<group>";
@ -3419,6 +3430,7 @@
87FBAE0024A1EE67005EF293 /* AddressOrEnsNameLabel.swift */,
8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */,
874AF0822603405F00D613A5 /* LoadingIndicatorView.swift */,
87F4D41926C26C0700EFB9BC /* DropDownView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -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 */,

@ -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
}
}

@ -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
}
}

@ -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]
}
}

@ -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<T: DropDownItemType> {
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))
}
}

@ -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<T: DropDownItemType>: 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<T>
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<T>) {
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<T>) {
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)]
}
}
}

@ -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 %@";

@ -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 %@";

@ -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 %@";

@ -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 %@";

@ -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 %@";

@ -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 {

@ -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<DropDownView<SortTokensParam>>.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<SortTokensParam> = {
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<DropDownView<SortTokensParam>> = 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

@ -461,7 +461,7 @@ extension TokensViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
switch sections[section] {
case .walletSummary:
let header: GeneralTableViewSectionHeader<WalletSummaryView> = tableView.dequeueReusableHeaderFooterView()
let header: TokensViewController.GeneralTableViewSectionHeader<WalletSummaryView> = tableView.dequeueReusableHeaderFooterView()
header.subview = walletSummaryView
return header

@ -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<TokenObject> {
@ -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
}
}
}

@ -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
}
}

@ -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)
]
}
}

@ -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)
}
}

@ -350,7 +350,7 @@ class ButtonsBar: UIView {
}
}
private struct ButtonsBarViewModel {
struct ButtonsBarViewModel {
static let greenButton = ButtonsBarViewModel(
buttonBackgroundColor: Colors.appActionButtonGreen,

Loading…
Cancel
Save