|
|
|
@ -13,8 +13,10 @@ protocol TokensViewControllerDelegate: class { |
|
|
|
|
|
|
|
|
|
class TokensViewController: UIViewController { |
|
|
|
|
private static let filterViewHeight = CGFloat(50) |
|
|
|
|
private static let addHideTokensViewHeight = CGFloat(60) |
|
|
|
|
|
|
|
|
|
private enum Section { |
|
|
|
|
private enum Section: CaseIterable { |
|
|
|
|
case filters |
|
|
|
|
case addHideToken |
|
|
|
|
case tokens |
|
|
|
|
} |
|
|
|
@ -22,7 +24,7 @@ class TokensViewController: UIViewController { |
|
|
|
|
private let tokenCollection: TokenCollection |
|
|
|
|
private let assetDefinitionStore: AssetDefinitionStore |
|
|
|
|
private let eventsDataStore: EventsDataStoreProtocol |
|
|
|
|
private let sections: [Section] = [.addHideToken, .tokens] |
|
|
|
|
private let sections: [Section] = Section.allCases |
|
|
|
|
|
|
|
|
|
private var viewModel: TokensViewModel { |
|
|
|
|
didSet { |
|
|
|
@ -34,10 +36,36 @@ class TokensViewController: UIViewController { |
|
|
|
|
private let account: Wallet |
|
|
|
|
lazy private var tableViewFilterView = SegmentedControl(titles: TokensViewModel.segmentedControlTitles) |
|
|
|
|
lazy private var collectiblesCollectionViewFilterView = SegmentedControl(titles: TokensViewModel.segmentedControlTitles) |
|
|
|
|
private let tableView: UITableView |
|
|
|
|
private let tableViewRefreshControl = UIRefreshControl() |
|
|
|
|
private let collectiblesCollectionViewRefreshControl = UIRefreshControl() |
|
|
|
|
private let collectiblesCollectionView = { () -> UICollectionView in |
|
|
|
|
private lazy var tableView: UITableView = { |
|
|
|
|
let tableView = UITableView(frame: .zero, style: .plain) |
|
|
|
|
tableView.register(FungibleTokenViewCell.self) |
|
|
|
|
tableView.register(EthTokenViewCell.self) |
|
|
|
|
tableView.register(NonFungibleTokenViewCell.self) |
|
|
|
|
tableView.registerHeaderFooterView(TableViewSectionHeader.self) |
|
|
|
|
tableView.registerHeaderFooterView(ShowAddHideTokensView.self) |
|
|
|
|
tableView.estimatedRowHeight = 100 |
|
|
|
|
tableView.delegate = self |
|
|
|
|
tableView.dataSource = self |
|
|
|
|
tableView.tableFooterView = UIView.tableFooterToRemoveEmptyCellSeparators() |
|
|
|
|
tableView.separatorInset = .zero |
|
|
|
|
|
|
|
|
|
tableView.addSubview(tableViewRefreshControl) |
|
|
|
|
tableView.translatesAutoresizingMaskIntoConstraints = false |
|
|
|
|
|
|
|
|
|
return tableView |
|
|
|
|
}() |
|
|
|
|
private lazy var tableViewRefreshControl: UIRefreshControl = { |
|
|
|
|
let control = UIRefreshControl() |
|
|
|
|
control.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged) |
|
|
|
|
return control |
|
|
|
|
}() |
|
|
|
|
private lazy var collectiblesCollectionViewRefreshControl: UIRefreshControl = { |
|
|
|
|
let control = UIRefreshControl() |
|
|
|
|
control.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged) |
|
|
|
|
|
|
|
|
|
return control |
|
|
|
|
}() |
|
|
|
|
private lazy var collectiblesCollectionView: UICollectionView = { |
|
|
|
|
let layout = UICollectionViewFlowLayout() |
|
|
|
|
let numberOfColumns = CGFloat(3) |
|
|
|
|
let dimension = (UIScreen.main.bounds.size.width / numberOfColumns).rounded(.down) |
|
|
|
@ -46,7 +74,19 @@ class TokensViewController: UIViewController { |
|
|
|
|
layout.minimumInteritemSpacing = 0 |
|
|
|
|
layout.headerReferenceSize = .init(width: 100, height: TokensViewController.filterViewHeight) |
|
|
|
|
layout.sectionHeadersPinToVisibleBounds = true |
|
|
|
|
return UICollectionView(frame: .zero, collectionViewLayout: layout) |
|
|
|
|
|
|
|
|
|
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) |
|
|
|
|
collectionView.backgroundColor = viewModel.backgroundColor |
|
|
|
|
collectionView.translatesAutoresizingMaskIntoConstraints = false |
|
|
|
|
collectionView.alwaysBounceVertical = true |
|
|
|
|
collectionView.register(OpenSeaNonFungibleTokenViewCell.self) |
|
|
|
|
collectionView.registerSupplementaryView(CollectiblesCollectionViewHeader.self, of: UICollectionView.elementKindSectionHeader) |
|
|
|
|
collectionView.dataSource = self |
|
|
|
|
collectionView.isHidden = true |
|
|
|
|
collectionView.delegate = self |
|
|
|
|
collectionView.refreshControl = collectiblesCollectionViewRefreshControl |
|
|
|
|
|
|
|
|
|
return collectionView |
|
|
|
|
}() |
|
|
|
|
private var currentCollectiblesContractsDisplayed = [AlphaWallet.Address]() |
|
|
|
|
private let searchController: UISearchController |
|
|
|
@ -129,7 +169,6 @@ class TokensViewController: UIViewController { |
|
|
|
|
self.assetDefinitionStore = assetDefinitionStore |
|
|
|
|
self.eventsDataStore = eventsDataStore |
|
|
|
|
self.viewModel = TokensViewModel(filterTokensCoordinator: filterTokensCoordinator, tokens: [], tickers: .init()) |
|
|
|
|
tableView = UITableView(frame: .zero, style: .plain) |
|
|
|
|
searchController = UISearchController(searchResultsController: nil) |
|
|
|
|
|
|
|
|
|
super.init(nibName: nil, bundle: nil) |
|
|
|
@ -146,33 +185,7 @@ class TokensViewController: UIViewController { |
|
|
|
|
|
|
|
|
|
consoleButton.addTarget(self, action: #selector(openConsole), for: .touchUpInside) |
|
|
|
|
|
|
|
|
|
tableView.register(AddHideTokensCell.self) |
|
|
|
|
tableView.register(FungibleTokenViewCell.self) |
|
|
|
|
tableView.register(EthTokenViewCell.self) |
|
|
|
|
tableView.register(NonFungibleTokenViewCell.self) |
|
|
|
|
tableView.registerHeaderFooterView(TableViewSectionHeader.self) |
|
|
|
|
// tableView.estimatedRowHeight = 0 |
|
|
|
|
tableView.estimatedRowHeight = 100 |
|
|
|
|
tableView.delegate = self |
|
|
|
|
tableView.dataSource = self |
|
|
|
|
tableView.tableFooterView = UIView.tableFooterToRemoveEmptyCellSeparators() |
|
|
|
|
tableView.separatorInset = .zero |
|
|
|
|
|
|
|
|
|
tableViewRefreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged) |
|
|
|
|
tableView.addSubview(tableViewRefreshControl) |
|
|
|
|
tableView.translatesAutoresizingMaskIntoConstraints = false |
|
|
|
|
view.addSubview(tableView) |
|
|
|
|
|
|
|
|
|
collectiblesCollectionView.backgroundColor = viewModel.backgroundColor |
|
|
|
|
collectiblesCollectionView.translatesAutoresizingMaskIntoConstraints = false |
|
|
|
|
collectiblesCollectionView.alwaysBounceVertical = true |
|
|
|
|
collectiblesCollectionView.register(OpenSeaNonFungibleTokenViewCell.self) |
|
|
|
|
collectiblesCollectionView.registerSupplementaryView(CollectiblesCollectionViewHeader.self, of: UICollectionView.elementKindSectionHeader) |
|
|
|
|
collectiblesCollectionView.dataSource = self |
|
|
|
|
collectiblesCollectionView.isHidden = true |
|
|
|
|
collectiblesCollectionView.delegate = self |
|
|
|
|
collectiblesCollectionViewRefreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged) |
|
|
|
|
collectiblesCollectionView.refreshControl = collectiblesCollectionViewRefreshControl |
|
|
|
|
view.addSubview(collectiblesCollectionView) |
|
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([ |
|
|
|
@ -248,7 +261,7 @@ class TokensViewController: UIViewController { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
required init?(coder aDecoder: NSCoder) { |
|
|
|
|
fatalError("init(coder:) has not been implemented") |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func refreshView(viewModel: TokensViewModel) { |
|
|
|
@ -305,80 +318,92 @@ extension TokensViewController: StatefulViewController { |
|
|
|
|
extension TokensViewController: UITableViewDelegate { |
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
|
|
|
|
tableView.deselectRow(at: indexPath, animated: true) |
|
|
|
|
|
|
|
|
|
let token = viewModel.item(for: indexPath.row, section: indexPath.section) |
|
|
|
|
delegate?.didSelect(token: token, in: self) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { |
|
|
|
|
guard section == 0 else { return 0 } |
|
|
|
|
return TokensViewController.filterViewHeight |
|
|
|
|
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { |
|
|
|
|
guard section == 0 else { return nil } |
|
|
|
|
let header: TableViewSectionHeader = tableView.dequeueReusableHeaderFooterView() |
|
|
|
|
header.filterView = tableViewFilterView |
|
|
|
|
|
|
|
|
|
return header |
|
|
|
|
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { |
|
|
|
|
return 0.01 |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
extension TokensViewController: UITableViewDataSource { |
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
|
|
|
|
let section = sections[indexPath.section] |
|
|
|
|
switch section { |
|
|
|
|
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { |
|
|
|
|
switch sections[section] { |
|
|
|
|
case .filters: |
|
|
|
|
return TokensViewController.filterViewHeight |
|
|
|
|
case .addHideToken: |
|
|
|
|
return addHideTokenCell(forIndexPath: indexPath) |
|
|
|
|
return TokensViewController.addHideTokensViewHeight |
|
|
|
|
case .tokens: |
|
|
|
|
return tokenCell(forIndexPath: indexPath) |
|
|
|
|
return 0.01 |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private func addHideTokenCell(forIndexPath indexPath: IndexPath) -> UITableViewCell { |
|
|
|
|
let cell: AddHideTokensCell = tableView.dequeueReusableCell(for: indexPath) |
|
|
|
|
cell.delegate = self |
|
|
|
|
cell.configure() |
|
|
|
|
return cell |
|
|
|
|
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { |
|
|
|
|
switch sections[section] { |
|
|
|
|
case .filters: |
|
|
|
|
let header: TableViewSectionHeader = tableView.dequeueReusableHeaderFooterView() |
|
|
|
|
header.filterView = tableViewFilterView |
|
|
|
|
|
|
|
|
|
return header |
|
|
|
|
case .addHideToken: |
|
|
|
|
let header: ShowAddHideTokensView = tableView.dequeueReusableHeaderFooterView() |
|
|
|
|
header.delegate = self |
|
|
|
|
header.configure(viewModel: .init()) |
|
|
|
|
|
|
|
|
|
return header |
|
|
|
|
case .tokens: |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private func tokenCell(forIndexPath indexPath: IndexPath) -> UITableViewCell { |
|
|
|
|
let token = viewModel.item(for: indexPath.row, section: indexPath.section) |
|
|
|
|
let server = token.server |
|
|
|
|
let session = sessions[server] |
|
|
|
|
switch token.type { |
|
|
|
|
case .nativeCryptocurrency: |
|
|
|
|
let cell: EthTokenViewCell = tableView.dequeueReusableCell(for: indexPath) |
|
|
|
|
cell.configure( |
|
|
|
|
viewModel: .init( |
|
|
|
|
token: token, |
|
|
|
|
ticker: viewModel.ticker(for: token), |
|
|
|
|
currencyAmount: session.balanceCoordinator.viewModel.currencyAmount, |
|
|
|
|
currencyAmountWithoutSymbol: session.balanceCoordinator.viewModel.currencyAmountWithoutSymbol, |
|
|
|
|
server: server, |
|
|
|
|
assetDefinitionStore: assetDefinitionStore |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
return cell |
|
|
|
|
case .erc20: |
|
|
|
|
let cell: FungibleTokenViewCell = tableView.dequeueReusableCell(for: indexPath) |
|
|
|
|
cell.configure(viewModel: .init(token: token, server: server, assetDefinitionStore: assetDefinitionStore)) |
|
|
|
|
return cell |
|
|
|
|
case .erc721, .erc721ForTickets: |
|
|
|
|
let cell: NonFungibleTokenViewCell = tableView.dequeueReusableCell(for: indexPath) |
|
|
|
|
cell.configure(viewModel: .init(token: token, server: server, assetDefinitionStore: assetDefinitionStore)) |
|
|
|
|
return cell |
|
|
|
|
case .erc875: |
|
|
|
|
let cell: NonFungibleTokenViewCell = tableView.dequeueReusableCell(for: indexPath) |
|
|
|
|
cell.configure(viewModel: .init(token: token, server: server, assetDefinitionStore: assetDefinitionStore)) |
|
|
|
|
return cell |
|
|
|
|
extension TokensViewController: UITableViewDataSource { |
|
|
|
|
|
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
|
|
|
|
switch sections[indexPath.section] { |
|
|
|
|
case .addHideToken, .filters: |
|
|
|
|
return UITableViewCell() |
|
|
|
|
case .tokens: |
|
|
|
|
let token = viewModel.item(for: indexPath.row, section: indexPath.section) |
|
|
|
|
let server = token.server |
|
|
|
|
let session = sessions[server] |
|
|
|
|
|
|
|
|
|
switch token.type { |
|
|
|
|
case .nativeCryptocurrency: |
|
|
|
|
let cell: EthTokenViewCell = tableView.dequeueReusableCell(for: indexPath) |
|
|
|
|
cell.configure(viewModel: .init( |
|
|
|
|
token: token, |
|
|
|
|
ticker: viewModel.ticker(for: token), |
|
|
|
|
currencyAmount: session.balanceCoordinator.viewModel.currencyAmount, |
|
|
|
|
currencyAmountWithoutSymbol: session.balanceCoordinator.viewModel.currencyAmountWithoutSymbol, |
|
|
|
|
server: server, |
|
|
|
|
assetDefinitionStore: assetDefinitionStore |
|
|
|
|
)) |
|
|
|
|
return cell |
|
|
|
|
case .erc20: |
|
|
|
|
let cell: FungibleTokenViewCell = tableView.dequeueReusableCell(for: indexPath) |
|
|
|
|
cell.configure(viewModel: .init(token: token, server: server, assetDefinitionStore: assetDefinitionStore)) |
|
|
|
|
return cell |
|
|
|
|
case .erc721, .erc721ForTickets: |
|
|
|
|
let cell: NonFungibleTokenViewCell = tableView.dequeueReusableCell(for: indexPath) |
|
|
|
|
cell.configure(viewModel: .init(token: token, server: server, assetDefinitionStore: assetDefinitionStore)) |
|
|
|
|
return cell |
|
|
|
|
case .erc875: |
|
|
|
|
let cell: NonFungibleTokenViewCell = tableView.dequeueReusableCell(for: indexPath) |
|
|
|
|
cell.configure(viewModel: .init(token: token, server: server, assetDefinitionStore: assetDefinitionStore)) |
|
|
|
|
return cell |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
|
|
|
|
let section = sections[section] |
|
|
|
|
switch section { |
|
|
|
|
case .addHideToken: |
|
|
|
|
return 1 |
|
|
|
|
switch sections[section] { |
|
|
|
|
case .addHideToken, .filters: |
|
|
|
|
return 0 |
|
|
|
|
case .tokens: |
|
|
|
|
return viewModel.numberOfItems() |
|
|
|
|
} |
|
|
|
@ -389,9 +414,8 @@ extension TokensViewController: UITableViewDataSource { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { |
|
|
|
|
let section = sections[indexPath.section] |
|
|
|
|
switch section { |
|
|
|
|
case .addHideToken: |
|
|
|
|
switch sections[indexPath.section] { |
|
|
|
|
case .addHideToken, .filters: |
|
|
|
|
return nil |
|
|
|
|
case .tokens: |
|
|
|
|
return trailingSwipeActionsConfiguration(forRowAt: indexPath) |
|
|
|
@ -456,6 +480,11 @@ extension TokensViewController: SegmentedControlDelegate { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
extension TokensViewController: UICollectionViewDataSource { |
|
|
|
|
|
|
|
|
|
func numberOfSections(in collectionView: UICollectionView) -> Int { |
|
|
|
|
return 1 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
|
|
|
|
//Defensive check to make sure we don't return the wrong count. iOS might decide to load (the first time especially) the collection view at some point even if we don't switch to it, thus getting the wrong count and then at some point asking for a cell for those non-existent rows/items. E.g 10 tokens total, only 3 are collectibles and asked for the 6th cell |
|
|
|
|
switch viewModel.filter { |
|
|
|
|