Split fungible token view controller tabs to separate view controllers, add fungible token coordinator #5852
parent
264ee4ebd6
commit
e2ac16d3a3
@ -0,0 +1,172 @@ |
||||
// |
||||
// PriceAlertsViewController.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 12.05.2021. |
||||
// |
||||
|
||||
import UIKit |
||||
import AlphaWalletFoundation |
||||
import StatefulViewController |
||||
import Combine |
||||
|
||||
protocol PriceAlertsViewControllerDelegate: class { |
||||
func editAlertSelected(in viewController: PriceAlertsViewController, alert: PriceAlert) |
||||
func addAlertSelected(in viewController: PriceAlertsViewController) |
||||
} |
||||
|
||||
class PriceAlertsViewController: UIViewController { |
||||
private let viewModel: PriceAlertsViewModel |
||||
private lazy var dataSource = makeDataSource() |
||||
private lazy var tableView: UITableView = { |
||||
var tableView = UITableView.grouped |
||||
tableView.register(PriceAlertTableViewCell.self) |
||||
tableView.delegate = self |
||||
|
||||
return tableView |
||||
}() |
||||
private let updateAlert = PassthroughSubject<(value: Bool, indexPath: IndexPath), Never>() |
||||
private let removeAlert = PassthroughSubject<IndexPath, Never>() |
||||
private var cancelable = Set<AnyCancellable>() |
||||
|
||||
private lazy var addNotificationView: AddHideTokensView = { |
||||
let view = AddHideTokensView() |
||||
view.delegate = self |
||||
|
||||
return view |
||||
}() |
||||
weak var delegate: PriceAlertsViewControllerDelegate? |
||||
|
||||
init(viewModel: PriceAlertsViewModel) { |
||||
self.viewModel = viewModel |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
let stackView = [ |
||||
addNotificationView, |
||||
UIView.separator(), |
||||
tableView |
||||
].asStackView(axis: .vertical) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
view.addSubview(stackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
stackView.anchorsConstraint(to: view), |
||||
addNotificationView.heightAnchor.constraint(equalToConstant: DataEntry.Metric.Tokens.Filter.height) |
||||
]) |
||||
|
||||
emptyView = EmptyView.priceAlertsEmptyView() |
||||
} |
||||
|
||||
required init?(coder: NSCoder) { |
||||
return nil |
||||
} |
||||
|
||||
override func viewDidLoad() { |
||||
super.viewDidLoad() |
||||
|
||||
view.backgroundColor = Configuration.Color.Semantic.defaultViewBackground |
||||
bind(viewModel: viewModel) |
||||
} |
||||
|
||||
private func bind(viewModel: PriceAlertsViewModel) { |
||||
let input = PriceAlertsViewModelInput( |
||||
updateAlert: updateAlert.eraseToAnyPublisher(), |
||||
removeAlert: removeAlert.eraseToAnyPublisher()) |
||||
|
||||
let outout = viewModel.transform(input: input) |
||||
outout.viewState |
||||
.sink { [weak self, dataSource, addNotificationView] viewState in |
||||
dataSource.apply(viewState.snapshot, animatingDifferences: viewState.animatingDifferences) |
||||
addNotificationView.configure(viewModel: viewState.addNewAlertViewModel) |
||||
self?.endLoading() |
||||
}.store(in: &cancelable) |
||||
} |
||||
} |
||||
|
||||
extension PriceAlertsViewController: StatefulViewController { |
||||
func hasContent() -> Bool { |
||||
return dataSource.snapshot().numberOfItems > 0 |
||||
} |
||||
} |
||||
|
||||
extension PriceAlertsViewController { |
||||
private func makeDataSource() -> PriceAlertsViewModel.DataSource { |
||||
PriceAlertsViewModel.DataSource(tableView: tableView) { tableView, indexPath, viewModel -> PriceAlertTableViewCell in |
||||
let cell: PriceAlertTableViewCell = tableView.dequeueReusableCell(for: indexPath) |
||||
cell.delegate = self |
||||
cell.configure(viewModel: viewModel) |
||||
|
||||
return cell |
||||
} |
||||
} |
||||
} |
||||
|
||||
extension PriceAlertsViewController: PriceAlertTableViewCellDelegate { |
||||
func cell(_ cell: PriceAlertTableViewCell, didToggle value: Bool, indexPath: IndexPath) { |
||||
updateAlert.send((value, indexPath)) |
||||
} |
||||
} |
||||
|
||||
extension PriceAlertsViewController: UITableViewDelegate { |
||||
|
||||
//Hide the footer |
||||
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { |
||||
.leastNormalMagnitude |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { |
||||
nil |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { |
||||
.leastNormalMagnitude |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { |
||||
nil |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
||||
tableView.deselectRow(at: indexPath, animated: true) |
||||
|
||||
delegate?.editAlertSelected(in: self, alert: dataSource.item(at: indexPath).alert) |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { |
||||
return trailingSwipeActionsConfiguration(forRowAt: indexPath) |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { |
||||
return .delete |
||||
} |
||||
|
||||
private func trailingSwipeActionsConfiguration(forRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { |
||||
let title = R.string.localizable.delete() |
||||
let hideAction = UIContextualAction(style: .destructive, title: title) { [removeAlert, dataSource] (_, _, completion) in |
||||
var snapshot = dataSource.snapshot() |
||||
|
||||
let item = snapshot.itemIdentifiers(inSection: snapshot.sectionIdentifiers[indexPath.section])[indexPath.row] |
||||
snapshot.deleteItems([item]) |
||||
|
||||
dataSource.apply(snapshot, animatingDifferences: true) |
||||
|
||||
removeAlert.send(indexPath) |
||||
|
||||
completion(true) |
||||
} |
||||
|
||||
hideAction.backgroundColor = R.color.danger() |
||||
hideAction.image = R.image.hideToken() |
||||
let configuration = UISwipeActionsConfiguration(actions: [hideAction]) |
||||
configuration.performsFirstActionWithFullSwipe = true |
||||
|
||||
return configuration |
||||
} |
||||
} |
||||
|
||||
extension PriceAlertsViewController: AddHideTokensViewDelegate { |
||||
func view(_ view: AddHideTokensView, didSelectAddHideTokensButton sender: UIButton) { |
||||
delegate?.addAlertSelected(in: self) |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
// |
||||
// PriceAlertTableViewCellViewModel.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 19.11.2022. |
||||
// |
||||
|
||||
import UIKit |
||||
import AlphaWalletFoundation |
||||
|
||||
struct PriceAlertTableViewCellViewModel: Hashable { |
||||
let alert: PriceAlert |
||||
|
||||
let titleAttributedString: NSAttributedString |
||||
let icon: UIImage? |
||||
let isSelected: Bool |
||||
|
||||
init(alert: PriceAlert) { |
||||
self.alert = alert |
||||
titleAttributedString = .init(string: alert.title, attributes: [ |
||||
.font: Fonts.regular(size: 17), |
||||
.foregroundColor: Configuration.Color.Semantic.defaultForegroundText |
||||
]) |
||||
icon = alert.icon |
||||
isSelected = alert.isEnabled |
||||
} |
||||
} |
@ -0,0 +1,77 @@ |
||||
// |
||||
// PriceAlertTableViewCell.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 19.11.2022. |
||||
// |
||||
|
||||
import UIKit |
||||
|
||||
protocol PriceAlertTableViewCellDelegate: class { |
||||
func cell(_ cell: PriceAlertTableViewCell, didToggle value: Bool, indexPath: IndexPath) |
||||
} |
||||
|
||||
class PriceAlertTableViewCell: UITableViewCell { |
||||
private lazy var iconImageView: UIImageView = { |
||||
let imageView = UIImageView() |
||||
imageView.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
return imageView |
||||
}() |
||||
|
||||
private lazy var titleLabel: UILabel = { |
||||
let label = UILabel() |
||||
label.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
return label |
||||
}() |
||||
|
||||
private lazy var switchButton: UISwitch = { |
||||
let button = UISwitch() |
||||
button.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
return button |
||||
}() |
||||
|
||||
weak var delegate: PriceAlertTableViewCellDelegate? |
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { |
||||
super.init(style: style, reuseIdentifier: reuseIdentifier) |
||||
separatorInset = .zero |
||||
|
||||
let stackView = [ |
||||
.spacerWidth(16), |
||||
iconImageView, |
||||
.spacerWidth(16), |
||||
titleLabel, |
||||
.spacerWidth(16, flexible: true), |
||||
switchButton, |
||||
.spacerWidth(16), |
||||
].asStackView(axis: .horizontal, alignment: .center) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
contentView.addSubview(stackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
iconImageView.heightAnchor.constraint(equalToConstant: 18), |
||||
iconImageView.widthAnchor.constraint(equalToConstant: 18), |
||||
stackView.anchorsConstraint(to: contentView, edgeInsets: .init(top: 14, left: 0, bottom: 14, right: 0)) |
||||
]) |
||||
|
||||
switchButton.addTarget(self, action: #selector(toggleSelectionState), for: .valueChanged) |
||||
} |
||||
|
||||
required init?(coder: NSCoder) { |
||||
return nil |
||||
} |
||||
|
||||
func configure(viewModel: PriceAlertTableViewCellViewModel) { |
||||
iconImageView.image = viewModel.icon |
||||
titleLabel.attributedText = viewModel.titleAttributedString |
||||
switchButton.isEnabled = viewModel.isSelected |
||||
} |
||||
|
||||
@objc private func toggleSelectionState(_ sender: UISwitch) { |
||||
guard let indexPath = indexPath else { return } |
||||
delegate?.cell(self, didToggle: sender.isEnabled, indexPath: indexPath) |
||||
} |
||||
} |
@ -1,260 +0,0 @@ |
||||
// |
||||
// PriceAlertsPageView.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 12.05.2021. |
||||
// |
||||
|
||||
import UIKit |
||||
import AlphaWalletFoundation |
||||
|
||||
protocol PriceAlertsPageViewDelegate: class { |
||||
func editAlertSelected(in view: PriceAlertsPageView, alert: PriceAlert) |
||||
func addAlertSelected(in view: PriceAlertsPageView) |
||||
func removeAlert(in view: PriceAlertsPageView, indexPath: IndexPath) |
||||
func updateAlert(in view: PriceAlertsPageView, value: Bool, indexPath: IndexPath) |
||||
} |
||||
|
||||
class PriceAlertsPageView: UIView, PageViewType { |
||||
var title: String { viewModel.title } |
||||
|
||||
var rightBarButtonItem: UIBarButtonItem? |
||||
|
||||
private var viewModel: PriceAlertsPageViewModel |
||||
|
||||
private lazy var tableView: UITableView = { |
||||
var tableView = UITableView.grouped |
||||
tableView.register(PriceAlertTableViewCell.self) |
||||
tableView.delegate = self |
||||
tableView.dataSource = self |
||||
|
||||
return tableView |
||||
}() |
||||
|
||||
private lazy var statefulView: StatefulView<UITableView> = { |
||||
return .init(subview: tableView) |
||||
}() |
||||
|
||||
private lazy var addNotificationView: AddHideTokensView = { |
||||
let view = AddHideTokensView() |
||||
view.delegate = self |
||||
view.configure(viewModel: viewModel.addNewAlertViewModel) |
||||
|
||||
return view |
||||
}() |
||||
weak var delegate: PriceAlertsPageViewDelegate? |
||||
|
||||
init(viewModel: PriceAlertsPageViewModel) { |
||||
self.viewModel = viewModel |
||||
super.init(frame: .zero) |
||||
|
||||
translatesAutoresizingMaskIntoConstraints = false |
||||
let stackView = [ |
||||
addNotificationView.embededWithSeparator(top: 0), |
||||
statefulView |
||||
].asStackView(axis: .vertical) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
addSubview(stackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
stackView.anchorsConstraintSafeArea(to: self), |
||||
addNotificationView.heightAnchor.constraint(equalToConstant: DataEntry.Metric.Tokens.Filter.height) |
||||
]) |
||||
|
||||
statefulView.emptyView = EmptyView.activitiesEmptyView() |
||||
} |
||||
|
||||
required init?(coder: NSCoder) { |
||||
return nil |
||||
} |
||||
|
||||
func configure(viewModel: PriceAlertsPageViewModel) { |
||||
self.viewModel = viewModel |
||||
|
||||
reloadData() |
||||
statefulView.endLoading() |
||||
} |
||||
|
||||
deinit { |
||||
statefulView.resetStatefulStateToReleaseObjectToAvoidMemoryLeak() |
||||
} |
||||
|
||||
func reloadData() { |
||||
tableView.reloadData() |
||||
} |
||||
} |
||||
|
||||
extension PriceAlertsPageView: UITableViewDataSource { |
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int { |
||||
return 1 |
||||
} |
||||
|
||||
//Hide the footer |
||||
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { |
||||
.leastNormalMagnitude |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { |
||||
nil |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { |
||||
.leastNormalMagnitude |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { |
||||
nil |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
||||
return viewModel.alerts.count |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
||||
let cell: PriceAlertTableViewCell = tableView.dequeueReusableCell(for: indexPath) |
||||
cell.delegate = self |
||||
cell.configure(viewModel: .init(alert: viewModel.alerts[indexPath.row])) |
||||
|
||||
return cell |
||||
} |
||||
|
||||
} |
||||
|
||||
extension PriceAlertsPageView: PriceAlertTableViewCellDelegate { |
||||
func cell(_ cell: PriceAlertTableViewCell, didToggle value: Bool, indexPath: IndexPath) { |
||||
delegate?.updateAlert(in: self, value: value, indexPath: indexPath) |
||||
} |
||||
} |
||||
|
||||
extension PriceAlertsPageView: UITableViewDelegate { |
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
||||
tableView.deselectRow(at: indexPath, animated: true) |
||||
let alert = viewModel.alerts[indexPath.row] |
||||
|
||||
delegate?.editAlertSelected(in: self, alert: alert) |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { |
||||
return trailingSwipeActionsConfiguration(forRowAt: indexPath) |
||||
} |
||||
|
||||
private func trailingSwipeActionsConfiguration(forRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { |
||||
let title = R.string.localizable.delete() |
||||
let hideAction = UIContextualAction(style: .destructive, title: title) { [weak self] (_, _, completionHandler) in |
||||
guard let strongSelf = self else { return } |
||||
|
||||
strongSelf.viewModel.removeAlert(indexPath: indexPath) |
||||
strongSelf.tableView.deleteRows(at: [indexPath], with: .automatic) |
||||
strongSelf.statefulView.endLoading() |
||||
// NOTE: small delay for correct remove animation |
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { |
||||
strongSelf.delegate?.removeAlert(in: strongSelf, indexPath: indexPath) |
||||
} |
||||
|
||||
completionHandler(true) |
||||
} |
||||
|
||||
hideAction.backgroundColor = R.color.danger() |
||||
hideAction.image = R.image.hideToken() |
||||
let configuration = UISwipeActionsConfiguration(actions: [hideAction]) |
||||
configuration.performsFirstActionWithFullSwipe = true |
||||
|
||||
return configuration |
||||
} |
||||
} |
||||
|
||||
extension PriceAlertsPageView: AddHideTokensViewDelegate { |
||||
func view(_ view: AddHideTokensView, didSelectAddHideTokensButton sender: UIButton) { |
||||
delegate?.addAlertSelected(in: self) |
||||
} |
||||
} |
||||
|
||||
protocol PriceAlertTableViewCellDelegate: class { |
||||
func cell(_ cell: PriceAlertsPageView.PriceAlertTableViewCell, didToggle value: Bool, indexPath: IndexPath) |
||||
} |
||||
|
||||
extension PriceAlertsPageView { |
||||
|
||||
struct PriceAlertTableViewCellViewModel { |
||||
let titleAttributedString: NSAttributedString |
||||
let icon: UIImage? |
||||
let isSelected: Bool |
||||
|
||||
init(alert: PriceAlert) { |
||||
titleAttributedString = .init(string: alert.title, attributes: [ |
||||
.font: Fonts.regular(size: 17), |
||||
.foregroundColor: Colors.black |
||||
]) |
||||
icon = alert.icon |
||||
isSelected = alert.isEnabled |
||||
} |
||||
} |
||||
|
||||
class PriceAlertTableViewCell: UITableViewCell { |
||||
private lazy var iconImageView: UIImageView = { |
||||
let imageView = UIImageView() |
||||
imageView.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
return imageView |
||||
}() |
||||
|
||||
private lazy var titleLabel: UILabel = { |
||||
let label = UILabel() |
||||
label.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
return label |
||||
}() |
||||
|
||||
private lazy var switchButton: UISwitch = { |
||||
let button = UISwitch() |
||||
button.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
return button |
||||
}() |
||||
|
||||
weak var delegate: PriceAlertTableViewCellDelegate? |
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { |
||||
super.init(style: style, reuseIdentifier: reuseIdentifier) |
||||
separatorInset = .zero |
||||
|
||||
let stackView = [ |
||||
.spacerWidth(16), |
||||
iconImageView, |
||||
.spacerWidth(16), |
||||
titleLabel, |
||||
.spacerWidth(16, flexible: true), |
||||
switchButton, |
||||
.spacerWidth(16), |
||||
].asStackView(axis: .horizontal, alignment: .center) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
contentView.addSubview(stackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
iconImageView.heightAnchor.constraint(equalToConstant: 18), |
||||
iconImageView.widthAnchor.constraint(equalToConstant: 18), |
||||
stackView.anchorsConstraint(to: contentView, edgeInsets: .init(top: 14, left: 0, bottom: 14, right: 0)) |
||||
]) |
||||
|
||||
switchButton.addTarget(self, action: #selector(toggleSelectionState), for: .valueChanged) |
||||
} |
||||
|
||||
required init?(coder: NSCoder) { |
||||
return nil |
||||
} |
||||
|
||||
func configure(viewModel: PriceAlertTableViewCellViewModel) { |
||||
iconImageView.image = viewModel.icon |
||||
titleLabel.attributedText = viewModel.titleAttributedString |
||||
switchButton.isEnabled = viewModel.isSelected |
||||
} |
||||
|
||||
@objc private func toggleSelectionState(_ sender: UISwitch) { |
||||
guard let indexPath = indexPath else { return } |
||||
delegate?.cell(self, didToggle: sender.isEnabled, indexPath: indexPath) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,142 @@ |
||||
// |
||||
// TopTabBarViewController.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 30.11.2022. |
||||
// |
||||
|
||||
import UIKit |
||||
|
||||
protocol TopTabBarViewControllerDelegate: class { |
||||
func viewController(_ viewController: TopTabBarViewController, didSelectPage index: Int) |
||||
} |
||||
|
||||
class TopTabBarViewController: UIViewController { |
||||
|
||||
private lazy var tabBar: ScrollableSegmentedControl = { |
||||
let cellConfiguration = Style.ScrollableSegmentedControlCell.configuration |
||||
let controlConfiguration = Style.ScrollableSegmentedControl.configuration |
||||
let cells = titles.map { title in |
||||
ScrollableSegmentedControlCell(frame: .zero, title: title, configuration: cellConfiguration) |
||||
} |
||||
let control = ScrollableSegmentedControl(cells: cells, configuration: controlConfiguration) |
||||
control.setSelection(cellIndex: selectedIndex) |
||||
control.addTarget(self, action: #selector(didTapSegment), for: .touchUpInside) |
||||
|
||||
return control |
||||
}() |
||||
|
||||
private lazy var scrollView: UIScrollView = { |
||||
let scrollView = UIScrollView() |
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false |
||||
scrollView.isPagingEnabled = true |
||||
scrollView.isScrollEnabled = false |
||||
|
||||
return scrollView |
||||
}() |
||||
|
||||
private let stackView: UIStackView = { |
||||
let stackView = UIStackView() |
||||
stackView.axis = .horizontal |
||||
stackView.spacing = 0 |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
return stackView |
||||
}() |
||||
|
||||
weak var navigationDelegate: TopTabBarViewControllerDelegate? |
||||
|
||||
var selection: ControlSelection { |
||||
return tabBar.selectedSegment |
||||
} |
||||
private (set) var bottomAnchorConstraints: [NSLayoutConstraint] = [] |
||||
private let selectedIndex: Int |
||||
private let titles: [String] |
||||
|
||||
init(titles: [String], selectedIndex: Int = 0) { |
||||
self.titles = titles |
||||
self.selectedIndex = selectedIndex |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
view.addSubview(tabBar) |
||||
view.addSubview(scrollView) |
||||
scrollView.addSubview(stackView) |
||||
|
||||
bottomAnchorConstraints = [ |
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), |
||||
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor) |
||||
] |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
tabBar.heightAnchor.constraint(equalToConstant: DataEntry.Metric.TabBar.height), |
||||
tabBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
tabBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
tabBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), |
||||
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
scrollView.topAnchor.constraint(equalTo: tabBar.bottomAnchor), |
||||
|
||||
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), |
||||
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), |
||||
stackView.topAnchor.constraint(equalTo: tabBar.bottomAnchor), |
||||
] + bottomAnchorConstraints) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
return nil |
||||
} |
||||
|
||||
//NOTE: need to triggle initial selection state when view layout its subviews for first time |
||||
private var didLayoutSubviewsAtFirstTime: Bool = true |
||||
|
||||
func set(viewControllers: [UIViewController]) { |
||||
viewControllers.forEach { addChild($0) } |
||||
|
||||
let views = viewControllers.compactMap { $0.view } |
||||
stackView.addArrangedSubviews(views) |
||||
|
||||
let viewsHeights = views.flatMap { each -> [NSLayoutConstraint] in |
||||
return [ |
||||
each.widthAnchor.constraint(equalTo: view.widthAnchor), |
||||
each.heightAnchor.constraint(equalTo: scrollView.heightAnchor) |
||||
] |
||||
} |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
viewsHeights |
||||
]) |
||||
|
||||
viewControllers.forEach { $0.didMove(toParent: self) } |
||||
} |
||||
|
||||
override func viewDidLayoutSubviews() { |
||||
super.viewDidLayoutSubviews() |
||||
|
||||
guard scrollView.bounds.width != .zero && didLayoutSubviewsAtFirstTime else { |
||||
return |
||||
} |
||||
|
||||
didLayoutSubviewsAtFirstTime = false |
||||
selectTab(selection: tabBar.selectedSegment, animated: false) |
||||
} |
||||
|
||||
@objc private func didTapSegment(_ control: ScrollableSegmentedControl) { |
||||
selectTab(selection: control.selectedSegment, animated: true) |
||||
} |
||||
|
||||
private func selectTab(selection: ControlSelection, animated: Bool) { |
||||
let index: Int |
||||
switch selection { |
||||
case .selected(let value): |
||||
index = Int(value) |
||||
case .unselected: |
||||
index = 0 |
||||
} |
||||
|
||||
let offset = CGPoint(x: CGFloat(index) * scrollView.bounds.width, y: 0) |
||||
scrollView.setContentOffset(offset, animated: animated) |
||||
|
||||
navigationDelegate?.viewController(self, didSelectPage: index) |
||||
} |
||||
} |
@ -1,105 +0,0 @@ |
||||
// |
||||
// InfoPageView.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 12.05.2021. |
||||
// |
||||
|
||||
import UIKit |
||||
import Combine |
||||
import AlphaWalletFoundation |
||||
|
||||
protocol TokenInfoPageViewDelegate: class { |
||||
func didPressViewContractWebPage(forContract contract: AlphaWallet.Address, in tokenInfoPageView: TokenInfoPageView) |
||||
} |
||||
|
||||
class TokenInfoPageView: ScrollableStackView, PageViewType { |
||||
private lazy var headerView = FungibleTokenHeaderView(viewModel: viewModel.headerViewModel) |
||||
private lazy var chartView: TokenHistoryChartView = { |
||||
let chartView = TokenHistoryChartView(viewModel: viewModel.chartViewModel) |
||||
return chartView |
||||
}() |
||||
private let viewModel: TokenInfoPageViewModel |
||||
private var cancelable = Set<AnyCancellable>() |
||||
private let appear = PassthroughSubject<Void, Never>() |
||||
|
||||
weak var delegate: TokenInfoPageViewDelegate? |
||||
var rightBarButtonItem: UIBarButtonItem? |
||||
var title: String { return viewModel.tabTitle } |
||||
|
||||
init(viewModel: TokenInfoPageViewModel) { |
||||
self.viewModel = viewModel |
||||
super.init() |
||||
|
||||
headerView.delegate = self |
||||
|
||||
bind(viewModel: viewModel) |
||||
} |
||||
|
||||
func viewWillAppear() { |
||||
appear.send(()) |
||||
} |
||||
|
||||
private func generateSubviews(for viewTypes: [TokenInfoPageViewModel.ViewType]) { |
||||
stackView.removeAllArrangedSubviews() |
||||
|
||||
stackView.addArrangedSubview(headerView) |
||||
|
||||
for each in viewTypes { |
||||
switch each { |
||||
case .testnet: |
||||
stackView.addArrangedSubview(UIView.spacer(height: 40)) |
||||
stackView.addArrangedSubview(UIView.spacer(backgroundColor: Configuration.Color.Semantic.tableViewSeparator)) |
||||
|
||||
let view = TestnetTokenInfoView() |
||||
view.configure(viewModel: .init()) |
||||
|
||||
stackView.addArrangedSubview(view) |
||||
case .charts: |
||||
stackView.addArrangedSubview(chartView) |
||||
|
||||
stackView.addArrangedSubview(UIView.spacer(height: 10)) |
||||
stackView.addArrangedSubview(UIView.spacer(backgroundColor: Configuration.Color.Semantic.tableViewSeparator)) |
||||
stackView.addArrangedSubview(UIView.spacer(height: 10)) |
||||
case .field(let viewModel): |
||||
let indexPath = IndexPath(row: 0, section: 0) |
||||
let view = TokenAttributeView(indexPath: indexPath) |
||||
view.delegate = self |
||||
view.configure(viewModel: viewModel) |
||||
|
||||
stackView.addArrangedSubview(view) |
||||
case .header(let viewModel): |
||||
let view = TokenInfoHeaderView() |
||||
view.configure(viewModel: viewModel) |
||||
|
||||
stackView.addArrangedSubview(view) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func bind(viewModel: TokenInfoPageViewModel) { |
||||
let input = TokenInfoPageViewModelInput(appear: appear.eraseToAnyPublisher()) |
||||
let output = viewModel.transform(input: input) |
||||
output.viewState |
||||
.sink { [weak self] state in |
||||
self?.generateSubviews(for: state.views) |
||||
}.store(in: &cancelable) |
||||
} |
||||
|
||||
required init?(coder: NSCoder) { |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
extension TokenInfoPageView: TokenAttributeViewDelegate { |
||||
func didSelect(in view: TokenAttributeView) { |
||||
//no-op |
||||
} |
||||
} |
||||
|
||||
extension TokenInfoPageView: FungibleTokenHeaderViewDelegate { |
||||
|
||||
func didPressViewContractWebPage(inHeaderView: FungibleTokenHeaderView) { |
||||
delegate?.didPressViewContractWebPage(forContract: viewModel.token.contractAddress, in: self) |
||||
} |
||||
} |
@ -0,0 +1,212 @@ |
||||
// |
||||
// FungibleTokenCoordinator.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 19.11.2022. |
||||
// |
||||
|
||||
import Foundation |
||||
import AlphaWalletFoundation |
||||
import PromiseKit |
||||
import Combine |
||||
|
||||
protocol FungibleTokenCoordinatorDelegate: AnyObject, CanOpenURL { |
||||
func didTapSwap(swapTokenFlow: SwapTokenFlow, in coordinator: FungibleTokenCoordinator) |
||||
func didTapBridge(transactionType: TransactionType, service: TokenActionProvider, in coordinator: FungibleTokenCoordinator) |
||||
func didTapBuy(transactionType: TransactionType, service: TokenActionProvider, in coordinator: FungibleTokenCoordinator) |
||||
func didPress(for type: PaymentFlow, viewController: UIViewController, in coordinator: FungibleTokenCoordinator) |
||||
func didTap(transaction: TransactionInstance, viewController: UIViewController, in coordinator: FungibleTokenCoordinator) |
||||
func didTap(activity: Activity, viewController: UIViewController, in coordinator: FungibleTokenCoordinator) |
||||
|
||||
func didClose(in coordinator: FungibleTokenCoordinator) |
||||
} |
||||
|
||||
class FungibleTokenCoordinator: Coordinator { |
||||
private let keystore: Keystore |
||||
private let assetDefinitionStore: AssetDefinitionStore |
||||
private let analytics: AnalyticsLogger |
||||
private let tokenActionsProvider: SupportedTokenActionsProvider |
||||
private let coinTickersFetcher: CoinTickersFetcher |
||||
private let activitiesService: ActivitiesServiceType |
||||
private let sessions: ServerDictionary<WalletSession> |
||||
private let session: WalletSession |
||||
private let alertService: PriceAlertServiceType |
||||
private let tokensService: TokenBalanceRefreshable & TokenViewModelState & TokenHolderState |
||||
private let token: Token |
||||
private let navigationController: UINavigationController |
||||
private var cancelable = Set<AnyCancellable>() |
||||
|
||||
private lazy var rootViewController: FungibleTokenTabViewController = { |
||||
let viewModel = FungibleTokenTabViewModel(token: token, session: session, tokensService: tokensService, assetDefinitionStore: assetDefinitionStore) |
||||
let viewController = FungibleTokenTabViewController(viewModel: viewModel) |
||||
let viewControlers = viewModel.tabBarItems.map { buildViewController(tabBarItem: $0) } |
||||
viewController.set(viewControllers: viewControlers) |
||||
viewController.delegate = self |
||||
|
||||
return viewController |
||||
}() |
||||
|
||||
var coordinators: [Coordinator] = [] |
||||
weak var delegate: FungibleTokenCoordinatorDelegate? |
||||
|
||||
init(token: Token, |
||||
navigationController: UINavigationController, |
||||
session: WalletSession, |
||||
keystore: Keystore, |
||||
assetDefinitionStore: AssetDefinitionStore, |
||||
analytics: AnalyticsLogger, |
||||
tokenActionsProvider: SupportedTokenActionsProvider, |
||||
coinTickersFetcher: CoinTickersFetcher, |
||||
activitiesService: ActivitiesServiceType, |
||||
alertService: PriceAlertServiceType, |
||||
tokensService: TokenBalanceRefreshable & TokenViewModelState & TokenHolderState, |
||||
sessions: ServerDictionary<WalletSession>) { |
||||
self.token = token |
||||
self.navigationController = navigationController |
||||
self.sessions = sessions |
||||
self.tokensService = tokensService |
||||
self.session = session |
||||
self.keystore = keystore |
||||
self.assetDefinitionStore = assetDefinitionStore |
||||
self.analytics = analytics |
||||
self.tokenActionsProvider = tokenActionsProvider |
||||
self.coinTickersFetcher = coinTickersFetcher |
||||
self.activitiesService = activitiesService |
||||
self.alertService = alertService |
||||
} |
||||
|
||||
func start() { |
||||
rootViewController.hidesBottomBarWhenPushed = true |
||||
rootViewController.navigationItem.largeTitleDisplayMode = .never |
||||
|
||||
navigationController.pushViewController(rootViewController, animated: true) |
||||
} |
||||
|
||||
private func buildViewController(tabBarItem: FungibleTokenTabViewModel.TabBarItem) -> UIViewController { |
||||
switch tabBarItem { |
||||
case .details: |
||||
return buildDetailsViewController() |
||||
case .activities: |
||||
return buildActivitiesViewController() |
||||
case .alerts: |
||||
return buildAlertsViewController() |
||||
} |
||||
} |
||||
|
||||
private func buildActivitiesViewController() -> UIViewController { |
||||
let viewController = ActivitiesViewController(analytics: analytics, keystore: keystore, wallet: session.account, viewModel: .init(collection: .init(activities: [])), sessions: sessions, assetDefinitionStore: assetDefinitionStore) |
||||
viewController.delegate = self |
||||
|
||||
//FIXME: replace later with moving it to `ActivitiesViewController` |
||||
activitiesService.activitiesPublisher |
||||
.map { ActivityPageViewModel(activitiesViewModel: .init(collection: .init(activities: $0))) } |
||||
.receive(on: RunLoop.main) |
||||
.sink { [viewController] in |
||||
viewController.configure(viewModel: $0.activitiesViewModel) |
||||
}.store(in: &cancelable) |
||||
|
||||
activitiesService.start() |
||||
|
||||
return viewController |
||||
} |
||||
|
||||
private func buildAlertsViewController() -> UIViewController { |
||||
let viewModel = PriceAlertsViewModel(alertService: alertService, token: token) |
||||
let viewController = PriceAlertsViewController(viewModel: viewModel) |
||||
viewController.delegate = self |
||||
|
||||
return viewController |
||||
} |
||||
|
||||
private func buildDetailsViewController() -> UIViewController { |
||||
lazy var viewModel = FungibleTokenDetailsViewModel(token: token, coinTickersFetcher: coinTickersFetcher, tokensService: tokensService, session: session, assetDefinitionStore: assetDefinitionStore, tokenActionsProvider: tokenActionsProvider) |
||||
let viewController = FungibleTokenDetailsViewController(viewModel: viewModel) |
||||
viewController.delegate = self |
||||
|
||||
return viewController |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenCoordinator: FungibleTokenDetailsViewControllerDelegate { |
||||
func didTapSwap(swapTokenFlow: SwapTokenFlow, in viewController: FungibleTokenDetailsViewController) { |
||||
delegate?.didTapSwap(swapTokenFlow: swapTokenFlow, in: self) |
||||
} |
||||
|
||||
func didTapBridge(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenDetailsViewController) { |
||||
delegate?.didTapBridge(transactionType: .init(fungibleToken: token), service: service, in: self) |
||||
} |
||||
|
||||
func didTapBuy(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenDetailsViewController) { |
||||
delegate?.didTapBuy(transactionType: .init(fungibleToken: token), service: service, in: self) |
||||
} |
||||
|
||||
func didTapSend(for token: Token, in viewController: FungibleTokenDetailsViewController) { |
||||
delegate?.didPress(for: .send(type: .transaction(.init(fungibleToken: token))), viewController: viewController, in: self) |
||||
} |
||||
|
||||
func didTapReceive(for token: Token, in viewController: FungibleTokenDetailsViewController) { |
||||
delegate?.didPress(for: .request, viewController: viewController, in: self) |
||||
} |
||||
|
||||
func didTap(action: TokenInstanceAction, token: Token, in viewController: FungibleTokenDetailsViewController) { |
||||
guard let navigationController = viewController.navigationController else { return } |
||||
|
||||
let tokenHolder = token.getTokenHolder(assetDefinitionStore: assetDefinitionStore, forWallet: session.account) |
||||
delegate?.didPress(for: .send(type: .tokenScript(action: action, token: token, tokenHolder: tokenHolder)), viewController: navigationController, in: self) |
||||
} |
||||
|
||||
func didPressViewContractWebPage(forContract contract: AlphaWallet.Address, server: RPCServer, in viewController: UIViewController) { |
||||
delegate?.didPressViewContractWebPage(forContract: contract, server: server, in: viewController) |
||||
} |
||||
|
||||
func didPressViewContractWebPage(_ url: URL, in viewController: UIViewController) { |
||||
delegate?.didPressOpenWebPage(url, in: viewController) |
||||
} |
||||
|
||||
func didPressOpenWebPage(_ url: URL, in viewController: UIViewController) { |
||||
delegate?.didPressOpenWebPage(url, in: viewController) |
||||
} |
||||
|
||||
} |
||||
|
||||
extension FungibleTokenCoordinator: PriceAlertsViewControllerDelegate { |
||||
func editAlertSelected(in viewController: PriceAlertsViewController, alert: PriceAlert) { |
||||
let coordinator = EditPriceAlertCoordinator(navigationController: navigationController, configuration: .edit(alert), token: token, session: session, tokensService: tokensService, alertService: alertService) |
||||
addCoordinator(coordinator) |
||||
coordinator.delegate = self |
||||
coordinator.start() |
||||
} |
||||
|
||||
func addAlertSelected(in viewController: PriceAlertsViewController) { |
||||
let coordinator = EditPriceAlertCoordinator(navigationController: navigationController, configuration: .create, token: token, session: session, tokensService: tokensService, alertService: alertService) |
||||
addCoordinator(coordinator) |
||||
coordinator.delegate = self |
||||
coordinator.start() |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenCoordinator: ActivitiesViewControllerDelegate { |
||||
func didPressActivity(activity: AlphaWalletFoundation.Activity, in viewController: ActivitiesViewController) { |
||||
delegate?.didTap(activity: activity, viewController: viewController, in: self) |
||||
} |
||||
|
||||
func didPressTransaction(transaction: AlphaWalletFoundation.TransactionInstance, in viewController: ActivitiesViewController) { |
||||
delegate?.didTap(transaction: transaction, viewController: viewController, in: self) |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenCoordinator: EditPriceAlertCoordinatorDelegate { |
||||
func didClose(in coordinator: EditPriceAlertCoordinator) { |
||||
removeCoordinator(coordinator) |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenCoordinator: FungibleTokenTabViewControllerDelegate { |
||||
func didClose(in viewController: FungibleTokenTabViewController) { |
||||
delegate?.didClose(in: self) |
||||
} |
||||
|
||||
func open(url: URL) { |
||||
delegate?.didPressOpenWebPage(url, in: rootViewController) |
||||
} |
||||
} |
@ -0,0 +1,187 @@ |
||||
// |
||||
// FungibleTokenDetailsViewController.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 19.11.2022. |
||||
// |
||||
|
||||
import UIKit |
||||
import Combine |
||||
import AlphaWalletFoundation |
||||
|
||||
protocol FungibleTokenDetailsViewControllerDelegate: AnyObject, CanOpenURL { |
||||
func didTapSwap(swapTokenFlow: SwapTokenFlow, in viewController: FungibleTokenDetailsViewController) |
||||
func didTapBridge(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenDetailsViewController) |
||||
func didTapBuy(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenDetailsViewController) |
||||
func didTapSend(for token: Token, in viewController: FungibleTokenDetailsViewController) |
||||
func didTapReceive(for token: Token, in viewController: FungibleTokenDetailsViewController) |
||||
func didTap(action: TokenInstanceAction, token: Token, in viewController: FungibleTokenDetailsViewController) |
||||
} |
||||
|
||||
class FungibleTokenDetailsViewController: UIViewController { |
||||
private let containerView: ScrollableStackView = ScrollableStackView() |
||||
private let buttonsBar = HorizontalButtonsBar(configuration: .combined(buttons: 2)) |
||||
private lazy var headerView: FungibleTokenHeaderView = { |
||||
let view = FungibleTokenHeaderView(viewModel: viewModel.headerViewModel) |
||||
view.delegate = self |
||||
|
||||
return view |
||||
}() |
||||
private lazy var chartView: TokenHistoryChartView = { |
||||
let chartView = TokenHistoryChartView(viewModel: viewModel.chartViewModel) |
||||
return chartView |
||||
}() |
||||
|
||||
private let viewModel: FungibleTokenDetailsViewModel |
||||
private var cancelable = Set<AnyCancellable>() |
||||
private let willAppear = PassthroughSubject<Void, Never>() |
||||
|
||||
weak var delegate: FungibleTokenDetailsViewControllerDelegate? |
||||
|
||||
init(viewModel: FungibleTokenDetailsViewModel) { |
||||
self.viewModel = viewModel |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
let footerBar = ButtonsBarBackgroundView(buttonsBar: buttonsBar) |
||||
view.addSubview(footerBar) |
||||
view.addSubview(containerView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
containerView.topAnchor.constraint(equalTo: view.topAnchor), |
||||
containerView.bottomAnchor.constraint(equalTo: footerBar.topAnchor), |
||||
|
||||
footerBar.anchorsConstraint(to: view) |
||||
]) |
||||
|
||||
buttonsBar.viewController = self |
||||
} |
||||
|
||||
override func viewDidLoad() { |
||||
super.viewDidLoad() |
||||
|
||||
view.backgroundColor = Configuration.Color.Semantic.defaultViewBackground |
||||
|
||||
bind(viewModel: viewModel) |
||||
} |
||||
|
||||
override func viewWillAppear(_ animated: Bool) { |
||||
super.viewWillAppear(animated) |
||||
willAppear.send(()) |
||||
} |
||||
|
||||
private func buildSubviews(for viewTypes: [FungibleTokenDetailsViewModel.ViewType]) -> [UIView] { |
||||
var subviews: [UIView] = [] |
||||
subviews += [headerView] |
||||
|
||||
for each in viewTypes { |
||||
switch each { |
||||
case .testnet: |
||||
subviews += [UIView.spacer(height: 40)] |
||||
subviews += [UIView.spacer(backgroundColor: Configuration.Color.Semantic.tableViewSeparator)] |
||||
|
||||
let view = TestnetTokenInfoView() |
||||
view.configure(viewModel: .init()) |
||||
|
||||
subviews += [view] |
||||
case .charts: |
||||
subviews += [chartView] |
||||
|
||||
subviews += [UIView.spacer(height: 10)] |
||||
subviews += [UIView.spacer(backgroundColor: Configuration.Color.Semantic.tableViewSeparator)] |
||||
subviews += [UIView.spacer(height: 10)] |
||||
case .field(let viewModel): |
||||
let view = TokenAttributeView(indexPath: IndexPath(row: 0, section: 0)) |
||||
view.configure(viewModel: viewModel) |
||||
|
||||
subviews += [view] |
||||
case .header(let viewModel): |
||||
let view = TokenInfoHeaderView() |
||||
view.configure(viewModel: viewModel) |
||||
|
||||
subviews += [view] |
||||
} |
||||
} |
||||
|
||||
return subviews |
||||
} |
||||
|
||||
private func layoutSubviews(_ subviews: [UIView]) { |
||||
containerView.stackView.removeAllArrangedSubviews() |
||||
containerView.stackView.addArrangedSubviews(subviews) |
||||
} |
||||
|
||||
private func bind(viewModel: FungibleTokenDetailsViewModel) { |
||||
let input = FungibleTokenDetailsViewModelInput(willAppear: willAppear.eraseToAnyPublisher()) |
||||
let output = viewModel.transform(input: input) |
||||
output.viewState |
||||
.sink { [weak self] viewState in |
||||
guard let strongSelf = self else { return } |
||||
|
||||
strongSelf.layoutSubviews(strongSelf.buildSubviews(for: viewState.views)) |
||||
strongSelf.configureActionButtons(with: viewState.actions) |
||||
}.store(in: &cancelable) |
||||
} |
||||
|
||||
required init?(coder: NSCoder) { |
||||
return nil |
||||
} |
||||
|
||||
private func configureActionButtons(with actions: [TokenInstanceAction]) { |
||||
buttonsBar.configure(.combined(buttons: actions.count)) |
||||
|
||||
for (action, button) in zip(actions, buttonsBar.buttons) { |
||||
button.setTitle(action.name, for: .normal) |
||||
button.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) |
||||
|
||||
switch viewModel.buttonState(for: action) { |
||||
case .isEnabled(let isEnabled): |
||||
button.isEnabled = isEnabled |
||||
case .isDisplayed(let isDisplayed): |
||||
button.displayButton = isDisplayed |
||||
case .noOption: |
||||
continue |
||||
} |
||||
} |
||||
} |
||||
|
||||
@objc private func actionButtonTapped(sender: UIButton) { |
||||
for (action, button) in zip(viewModel.actions, buttonsBar.buttons) where button == sender { |
||||
switch action.type { |
||||
case .swap: |
||||
delegate?.didTapSwap(swapTokenFlow: .swapToken(token: viewModel.token), in: self) |
||||
case .erc20Send: |
||||
delegate?.didTapSend(for: viewModel.token, in: self) |
||||
case .erc20Receive: |
||||
delegate?.didTapReceive(for: viewModel.token, in: self) |
||||
case .nftRedeem, .nftSell, .nonFungibleTransfer: |
||||
break |
||||
case .tokenScript: |
||||
if let message = viewModel.tokenScriptWarningMessage(for: action) { |
||||
guard case .warning(let string) = message else { return } |
||||
show(message: string) |
||||
} else { |
||||
delegate?.didTap(action: action, token: viewModel.token, in: self) |
||||
} |
||||
case .bridge(let service): |
||||
delegate?.didTapBridge(for: viewModel.token, service: service, in: self) |
||||
case .buy(let service): |
||||
delegate?.didTapBuy(for: viewModel.token, service: service, in: self) |
||||
} |
||||
break |
||||
} |
||||
} |
||||
|
||||
private func show(message: String) { |
||||
UIAlertController.alert(message: message, alertButtonTitles: [R.string.localizable.oK()], alertButtonStyles: [.default], viewController: self) |
||||
} |
||||
|
||||
} |
||||
|
||||
extension FungibleTokenDetailsViewController: FungibleTokenHeaderViewDelegate { |
||||
|
||||
func didPressViewContractWebPage(inHeaderView: FungibleTokenHeaderView) { |
||||
delegate?.didPressViewContractWebPage(forContract: viewModel.token.contractAddress, server: viewModel.token.server, in: self) |
||||
} |
||||
} |
@ -0,0 +1,94 @@ |
||||
// |
||||
// FungibleTokenTabViewController.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 20.11.2022. |
||||
// |
||||
|
||||
import UIKit |
||||
import Combine |
||||
import AlphaWalletFoundation |
||||
|
||||
protocol FungibleTokenTabViewControllerDelegate: AnyObject, CanOpenURL2 { |
||||
func didClose(in viewController: FungibleTokenTabViewController) |
||||
} |
||||
|
||||
class FungibleTokenTabViewController: TopTabBarViewController { |
||||
private let viewModel: FungibleTokenTabViewModel |
||||
private var cancelable = Set<AnyCancellable>() |
||||
private let willAppear = PassthroughSubject<Void, Never>() |
||||
|
||||
weak var delegate: FungibleTokenTabViewControllerDelegate? |
||||
|
||||
init(viewModel: FungibleTokenTabViewModel) { |
||||
self.viewModel = viewModel |
||||
super.init(titles: viewModel.tabBarItems.map { $0.description }) |
||||
} |
||||
|
||||
override func viewDidLoad() { |
||||
super.viewDidLoad() |
||||
|
||||
view.backgroundColor = Configuration.Color.Semantic.defaultViewBackground |
||||
updateNavigationRightBarButtons(tokenScriptFileStatusHandler: viewModel.tokenScriptFileStatusHandler) |
||||
bind(viewModel: viewModel) |
||||
} |
||||
|
||||
override func viewWillAppear(_ animated: Bool) { |
||||
super.viewWillAppear(animated) |
||||
hideNavigationBarTopSeparatorLine() |
||||
willAppear.send(()) |
||||
} |
||||
|
||||
private func bind(viewModel: FungibleTokenTabViewModel) { |
||||
let input = FungibleTokenTabViewModelInput(willAppear: willAppear.eraseToAnyPublisher()) |
||||
let output = viewModel.transform(input: input) |
||||
output.viewState |
||||
.map { $0.title } |
||||
.assign(to: \.title, on: navigationItem, ownership: .weak) |
||||
.store(in: &cancelable) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
private func updateNavigationRightBarButtons(tokenScriptFileStatusHandler xmlHandler: XMLHandler) { |
||||
if Features.default.isAvailable(.isTokenScriptSignatureStatusEnabled) { |
||||
let tokenScriptStatusPromise = xmlHandler.tokenScriptStatus |
||||
if tokenScriptStatusPromise.isPending { |
||||
let label: UIBarButtonItem = .init(title: R.string.localizable.tokenScriptVerifying(), style: .plain, target: nil, action: nil) |
||||
navigationItem.rightBarButtonItem = label |
||||
|
||||
tokenScriptStatusPromise.done { [weak self] _ in |
||||
self?.updateNavigationRightBarButtons(tokenScriptFileStatusHandler: xmlHandler) |
||||
}.cauterize() |
||||
} |
||||
|
||||
if let server = xmlHandler.server, let status = tokenScriptStatusPromise.value, server.matches(server: viewModel.session.server) { |
||||
switch status { |
||||
case .type0NoTokenScript: |
||||
navigationItem.rightBarButtonItem = nil |
||||
case .type1GoodTokenScriptSignatureGoodOrOptional, .type2BadTokenScript: |
||||
let button = createTokenScriptFileStatusButton(withStatus: status, urlOpener: self) |
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: button) |
||||
} |
||||
} else { |
||||
navigationItem.rightBarButtonItem = nil |
||||
} |
||||
} else { |
||||
//no-op |
||||
} |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenTabViewController: PopNotifiable { |
||||
func didPopViewController(animated: Bool) { |
||||
delegate?.didClose(in: self) |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenTabViewController: CanOpenURL2 { |
||||
func open(url: URL) { |
||||
delegate?.open(url: url) |
||||
} |
||||
} |
@ -1,248 +0,0 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
import Combine |
||||
import AlphaWalletFoundation |
||||
|
||||
protocol FungibleTokenViewControllerDelegate: class, CanOpenURL { |
||||
func didTapSwap(swapTokenFlow: SwapTokenFlow, in viewController: FungibleTokenViewController) |
||||
func didTapBridge(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenViewController) |
||||
func didTapBuy(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenViewController) |
||||
func didTapSend(for token: Token, in viewController: FungibleTokenViewController) |
||||
func didTapReceive(for token: Token, in viewController: FungibleTokenViewController) |
||||
func didTap(transaction: TransactionInstance, in viewController: FungibleTokenViewController) |
||||
func didTap(activity: Activity, in viewController: FungibleTokenViewController) |
||||
func didTap(action: TokenInstanceAction, token: Token, in viewController: FungibleTokenViewController) |
||||
func didTapAddAlert(for token: Token, in viewController: FungibleTokenViewController) |
||||
func didTapEditAlert(for token: Token, alert: PriceAlert, in viewController: FungibleTokenViewController) |
||||
func didClose(in viewController: FungibleTokenViewController) |
||||
} |
||||
|
||||
class FungibleTokenViewController: UIViewController { |
||||
private var viewModel: FungibleTokenViewModel |
||||
private let buttonsBar = HorizontalButtonsBar(configuration: .combined(buttons: 2)) |
||||
private lazy var tokenInfoPageView: TokenInfoPageView = { |
||||
let view = TokenInfoPageView(viewModel: viewModel.tokenInfoPageViewModel) |
||||
view.delegate = self |
||||
|
||||
return view |
||||
}() |
||||
private lazy var activitiesPageView: ActivitiesPageView = { |
||||
return ActivitiesPageView(analytics: analytics, keystore: keystore, wallet: viewModel.wallet, viewModel: .init(activitiesViewModel: .init(collection: .init())), sessions: sessions, assetDefinitionStore: viewModel.assetDefinitionStore) |
||||
}() |
||||
private lazy var alertsPageView: PriceAlertsPageView = { |
||||
return PriceAlertsPageView(viewModel: .init(alerts: [])) |
||||
}() |
||||
private let activitiesService: ActivitiesServiceType |
||||
private let analytics: AnalyticsLogger |
||||
private let keystore: Keystore |
||||
private var cancelable = Set<AnyCancellable>() |
||||
private let appear = PassthroughSubject<Void, Never>() |
||||
private let updateAlert = PassthroughSubject<(value: Bool, indexPath: IndexPath), Never>() |
||||
private let removeAlert = PassthroughSubject<IndexPath, Never>() |
||||
private let sessions: ServerDictionary<WalletSession> |
||||
weak var delegate: FungibleTokenViewControllerDelegate? |
||||
|
||||
init(keystore: Keystore, analytics: AnalyticsLogger, viewModel: FungibleTokenViewModel, activitiesService: ActivitiesServiceType, sessions: ServerDictionary<WalletSession>) { |
||||
self.sessions = sessions |
||||
self.viewModel = viewModel |
||||
self.keystore = keystore |
||||
self.activitiesService = activitiesService |
||||
self.analytics = analytics |
||||
|
||||
super.init(nibName: nil, bundle: nil) |
||||
hidesBottomBarWhenPushed = true |
||||
|
||||
let footerBar = ButtonsBarBackgroundView(buttonsBar: buttonsBar) |
||||
let pageWithFooter = PageViewWithFooter(pageView: tokenInfoPageView, footerBar: footerBar) |
||||
let pages: [PageViewType] |
||||
if Features.default.isAvailable(.isAlertsEnabled) && viewModel.hasCoinTicker { |
||||
pages = [pageWithFooter, activitiesPageView, alertsPageView] |
||||
} else { |
||||
pages = [pageWithFooter, activitiesPageView] |
||||
} |
||||
|
||||
let containerView = PagesContainerView(pages: pages) |
||||
|
||||
view.addSubview(containerView) |
||||
NSLayoutConstraint.activate([containerView.anchorsConstraint(to: view)]) |
||||
|
||||
navigationItem.largeTitleDisplayMode = .never |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
return nil |
||||
} |
||||
|
||||
override func viewDidLoad() { |
||||
super.viewDidLoad() |
||||
|
||||
activitiesPageView.delegate = self |
||||
alertsPageView.delegate = self |
||||
buttonsBar.viewController = self |
||||
|
||||
bind(viewModel: viewModel) |
||||
} |
||||
|
||||
override func viewWillAppear(_ animated: Bool) { |
||||
super.viewWillAppear(animated) |
||||
hideNavigationBarTopSeparatorLine() |
||||
|
||||
appear.send(()) |
||||
tokenInfoPageView.viewWillAppear() |
||||
} |
||||
|
||||
override func viewWillDisappear(_ animated: Bool) { |
||||
super.viewWillDisappear(animated) |
||||
showNavigationBarTopSeparatorLine() |
||||
} |
||||
|
||||
private func bind(viewModel: FungibleTokenViewModel) { |
||||
view.backgroundColor = viewModel.backgroundColor |
||||
|
||||
updateNavigationRightBarButtons(tokenScriptFileStatusHandler: viewModel.tokenScriptFileStatusHandler) |
||||
|
||||
let input = FungibleTokenViewModelInput(appear: appear.eraseToAnyPublisher(), |
||||
updateAlert: updateAlert.eraseToAnyPublisher(), |
||||
removeAlert: removeAlert.eraseToAnyPublisher()) |
||||
|
||||
let output = viewModel.transform(input: input) |
||||
output.viewState |
||||
.sink { [weak self, navigationItem] state in |
||||
navigationItem.title = state.title |
||||
self?.configureActionButtons(with: state.actions) |
||||
}.store(in: &cancelable) |
||||
|
||||
output.activities |
||||
.sink { [weak activitiesPageView] in activitiesPageView?.configure(viewModel: $0) } |
||||
.store(in: &cancelable) |
||||
|
||||
output.alerts |
||||
.sink { [weak alertsPageView] in alertsPageView?.configure(viewModel: $0) } |
||||
.store(in: &cancelable) |
||||
} |
||||
|
||||
private func configureActionButtons(with actions: [TokenInstanceAction]) { |
||||
buttonsBar.configure(.combined(buttons: actions.count)) |
||||
|
||||
for (action, button) in zip(actions, buttonsBar.buttons) { |
||||
button.setTitle(action.name, for: .normal) |
||||
button.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) |
||||
|
||||
switch viewModel.buttonState(for: action) { |
||||
case .isEnabled(let isEnabled): |
||||
button.isEnabled = isEnabled |
||||
case .isDisplayed(let isDisplayed): |
||||
button.displayButton = isDisplayed |
||||
case .noOption: |
||||
continue |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func updateNavigationRightBarButtons(tokenScriptFileStatusHandler xmlHandler: XMLHandler) { |
||||
if Features.default.isAvailable(.isTokenScriptSignatureStatusEnabled) { |
||||
let tokenScriptStatusPromise = xmlHandler.tokenScriptStatus |
||||
if tokenScriptStatusPromise.isPending { |
||||
let label: UIBarButtonItem = .init(title: R.string.localizable.tokenScriptVerifying(), style: .plain, target: nil, action: nil) |
||||
navigationItem.rightBarButtonItem = label |
||||
|
||||
tokenScriptStatusPromise.done { [weak self] _ in |
||||
self?.updateNavigationRightBarButtons(tokenScriptFileStatusHandler: xmlHandler) |
||||
}.cauterize() |
||||
} |
||||
|
||||
if let server = xmlHandler.server, let status = tokenScriptStatusPromise.value, server.matches(server: viewModel.session.server) { |
||||
switch status { |
||||
case .type0NoTokenScript: |
||||
navigationItem.rightBarButtonItem = nil |
||||
case .type1GoodTokenScriptSignatureGoodOrOptional, .type2BadTokenScript: |
||||
let button = createTokenScriptFileStatusButton(withStatus: status, urlOpener: self) |
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: button) |
||||
} |
||||
} else { |
||||
navigationItem.rightBarButtonItem = nil |
||||
} |
||||
} else { |
||||
//no-op |
||||
} |
||||
} |
||||
|
||||
@objc private func actionButtonTapped(sender: UIButton) { |
||||
let actions = viewModel.actions |
||||
for (action, button) in zip(actions, buttonsBar.buttons) where button == sender { |
||||
switch action.type { |
||||
case .swap: |
||||
delegate?.didTapSwap(swapTokenFlow: .swapToken(token: viewModel.token), in: self) |
||||
case .erc20Send: |
||||
delegate?.didTapSend(for: viewModel.token, in: self) |
||||
case .erc20Receive: |
||||
delegate?.didTapReceive(for: viewModel.token, in: self) |
||||
case .nftRedeem, .nftSell, .nonFungibleTransfer: |
||||
break |
||||
case .tokenScript: |
||||
if let message = viewModel.tokenScriptWarningMessage(for: action) { |
||||
guard case .warning(let string) = message else { return } |
||||
show(message: string) |
||||
} else { |
||||
delegate?.didTap(action: action, token: viewModel.token, in: self) |
||||
} |
||||
case .bridge(let service): |
||||
delegate?.didTapBridge(for: viewModel.token, service: service, in: self) |
||||
case .buy(let service): |
||||
delegate?.didTapBuy(for: viewModel.token, service: service, in: self) |
||||
} |
||||
break |
||||
} |
||||
} |
||||
|
||||
private func show(message: String) { |
||||
UIAlertController.alert(message: message, alertButtonTitles: [R.string.localizable.oK()], alertButtonStyles: [.default], viewController: self) |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenViewController: CanOpenURL2 { |
||||
func open(url: URL) { |
||||
delegate?.didPressOpenWebPage(url, in: self) |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenViewController: TokenInfoPageViewDelegate { |
||||
func didPressViewContractWebPage(forContract contract: AlphaWallet.Address, in tokenInfoPageView: TokenInfoPageView) { |
||||
delegate?.didPressViewContractWebPage(forContract: contract, server: viewModel.session.server, in: self) |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenViewController: PriceAlertsPageViewDelegate { |
||||
func addAlertSelected(in view: PriceAlertsPageView) { |
||||
delegate?.didTapAddAlert(for: viewModel.token, in: self) |
||||
} |
||||
|
||||
func editAlertSelected(in view: PriceAlertsPageView, alert: PriceAlert) { |
||||
delegate?.didTapEditAlert(for: viewModel.token, alert: alert, in: self) |
||||
} |
||||
|
||||
func removeAlert(in view: PriceAlertsPageView, indexPath: IndexPath) { |
||||
removeAlert.send(indexPath) |
||||
} |
||||
|
||||
func updateAlert(in view: PriceAlertsPageView, value: Bool, indexPath: IndexPath) { |
||||
updateAlert.send((value: value, indexPath: indexPath)) |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenViewController: ActivitiesPageViewDelegate { |
||||
func didTap(activity: Activity, in view: ActivitiesPageView) { |
||||
delegate?.didTap(activity: activity, in: self) |
||||
} |
||||
|
||||
func didTap(transaction: TransactionInstance, in view: ActivitiesPageView) { |
||||
delegate?.didTap(transaction: transaction, in: self) |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenViewController: PopNotifiable { |
||||
func didPopViewController(animated: Bool) { |
||||
delegate?.didClose(in: self) |
||||
} |
||||
} |
@ -0,0 +1,300 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
import Combine |
||||
import AlphaWalletFoundation |
||||
|
||||
struct FungibleTokenDetailsViewModelInput { |
||||
let willAppear: AnyPublisher<Void, Never> |
||||
} |
||||
|
||||
struct FungibleTokenDetailsViewModelOutput { |
||||
let viewState: AnyPublisher<FungibleTokenDetailsViewModel.ViewState, Never> |
||||
} |
||||
|
||||
final class FungibleTokenDetailsViewModel { |
||||
private var chartHistoriesSubject: CurrentValueSubject<[ChartHistoryPeriod: ChartHistory], Never> = .init([:]) |
||||
private let coinTickersFetcher: CoinTickersFetcher |
||||
private let tokensService: TokenViewModelState |
||||
private var cancelable = Set<AnyCancellable>() |
||||
private var chartHistories: [ChartHistoryPeriod: ChartHistory] { chartHistoriesSubject.value } |
||||
private lazy var coinTicker: AnyPublisher<CoinTicker?, Never> = { |
||||
return tokensService.tokenViewModelPublisher(for: token) |
||||
.map { $0?.balance.ticker } |
||||
.eraseToAnyPublisher() |
||||
}() |
||||
private lazy var tokenHolder: TokenHolder = token.getTokenHolder(assetDefinitionStore: assetDefinitionStore, forWallet: session.account) |
||||
private let session: WalletSession |
||||
private let assetDefinitionStore: AssetDefinitionStore |
||||
private let tokenActionsProvider: SupportedTokenActionsProvider |
||||
private (set) var actions: [TokenInstanceAction] = [] |
||||
|
||||
let token: Token |
||||
lazy var chartViewModel = TokenHistoryChartViewModel(chartHistories: chartHistoriesSubject.eraseToAnyPublisher(), coinTicker: coinTicker) |
||||
lazy var headerViewModel = FungibleTokenHeaderViewModel(token: token, tokensService: tokensService) |
||||
var wallet: Wallet { session.account } |
||||
|
||||
init(token: Token, coinTickersFetcher: CoinTickersFetcher, tokensService: TokenViewModelState, session: WalletSession, assetDefinitionStore: AssetDefinitionStore, tokenActionsProvider: SupportedTokenActionsProvider) { |
||||
self.tokenActionsProvider = tokenActionsProvider |
||||
self.session = session |
||||
self.assetDefinitionStore = assetDefinitionStore |
||||
self.tokensService = tokensService |
||||
self.coinTickersFetcher = coinTickersFetcher |
||||
self.token = token |
||||
} |
||||
|
||||
func transform(input: FungibleTokenDetailsViewModelInput) -> FungibleTokenDetailsViewModelOutput { |
||||
input.willAppear.flatMapLatest { [coinTickersFetcher, token] _ in |
||||
coinTickersFetcher.fetchChartHistories(for: .init(token: token), force: false, periods: ChartHistoryPeriod.allCases) |
||||
}.print("xxx.chartHistories").assign(to: \.value, on: chartHistoriesSubject) |
||||
.store(in: &cancelable) |
||||
|
||||
let viewTypes = Publishers.CombineLatest(coinTicker, chartHistoriesSubject) |
||||
.compactMap { [weak self] ticker, _ in self?.buildViewTypes(for: ticker) } |
||||
|
||||
let viewState = Publishers.CombineLatest(tokenActionsPublisher(), viewTypes) |
||||
.map { FungibleTokenDetailsViewModel.ViewState(actions: $0, views: $1) } |
||||
.eraseToAnyPublisher() |
||||
|
||||
return .init(viewState: viewState) |
||||
} |
||||
|
||||
private func tokenActionsPublisher() -> AnyPublisher<[TokenInstanceAction], Never> { |
||||
let whenTokenHolderHasChanged = tokenHolder.objectWillChange |
||||
.map { [tokensService, token] _ in tokensService.tokenViewModel(for: token) } |
||||
.receive(on: RunLoop.main) |
||||
.eraseToAnyPublisher() |
||||
|
||||
let whenTokenActionsHasChanged = tokenActionsProvider.objectWillChange |
||||
.map { [tokensService, token] _ in tokensService.tokenViewModel(for: token) } |
||||
.receive(on: RunLoop.main) |
||||
.eraseToAnyPublisher() |
||||
|
||||
let tokenViewModel = tokensService.tokenViewModelPublisher(for: token) |
||||
|
||||
return Publishers.MergeMany(tokenViewModel, whenTokenHolderHasChanged, whenTokenActionsHasChanged) |
||||
.compactMap { _ in self.buildTokenActions() } |
||||
.handleEvents(receiveOutput: { self.actions = $0 }) |
||||
.eraseToAnyPublisher() |
||||
} |
||||
|
||||
private func buildTokenActions() -> [TokenInstanceAction] { |
||||
let xmlHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore) |
||||
let actionsFromTokenScript = xmlHandler.actions |
||||
infoLog("[TokenScript] actions names: \(actionsFromTokenScript.map(\.name))") |
||||
if actionsFromTokenScript.isEmpty { |
||||
switch token.type { |
||||
case .erc875, .erc721, .erc721ForTickets, .erc1155: |
||||
return [] |
||||
case .erc20, .nativeCryptocurrency: |
||||
let actions: [TokenInstanceAction] = [ |
||||
.init(type: .erc20Send), |
||||
.init(type: .erc20Receive) |
||||
] |
||||
|
||||
return actions + tokenActionsProvider.actions(token: token) |
||||
} |
||||
} else { |
||||
switch token.type { |
||||
case .erc875, .erc721, .erc721ForTickets, .erc1155: |
||||
return [] |
||||
case .erc20: |
||||
return actionsFromTokenScript + tokenActionsProvider.actions(token: token) |
||||
case .nativeCryptocurrency: |
||||
//TODO we should support retrieval of XML (and XMLHandler) based on address + server. For now, this is only important for native cryptocurrency. So might be ok to check like this for now |
||||
if let server = xmlHandler.server, server.matches(server: token.server) { |
||||
return actionsFromTokenScript + tokenActionsProvider.actions(token: token) |
||||
} else { |
||||
//TODO .erc20Send and .erc20Receive names aren't appropriate |
||||
let actions: [TokenInstanceAction] = [ |
||||
.init(type: .erc20Send), |
||||
.init(type: .erc20Receive) |
||||
] |
||||
|
||||
return actions + tokenActionsProvider.actions(token: token) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func buildViewTypes(for ticker: CoinTicker?) -> [FungibleTokenDetailsViewModel.ViewType] { |
||||
var views: [FungibleTokenDetailsViewModel.ViewType] = [] |
||||
|
||||
if token.server.isTestnet { |
||||
views = [ |
||||
.testnet |
||||
] |
||||
} else { |
||||
views = [ |
||||
.charts, |
||||
.header(viewModel: .init(title: R.string.localizable.tokenInfoHeaderPerformance())), |
||||
.field(viewModel: dayViewModel), |
||||
.field(viewModel: weekViewModel), |
||||
.field(viewModel: monthViewModel), |
||||
.field(viewModel: yearViewModel), |
||||
|
||||
.header(viewModel: .init(title: R.string.localizable.tokenInfoHeaderStats())), |
||||
.field(viewModel: markerCapViewModel(for: ticker)), |
||||
.field(viewModel: yearLowViewModel), |
||||
.field(viewModel: yearHighViewModel) |
||||
] |
||||
} |
||||
|
||||
return views |
||||
} |
||||
|
||||
func tokenScriptWarningMessage(for action: TokenInstanceAction) -> TokenScriptWarningMessage? { |
||||
let fungibleBalance = tokensService.tokenViewModel(for: token)?.balance.value |
||||
if let selection = action.activeExcludingSelection(selectedTokenHolders: [tokenHolder], forWalletAddress: wallet.address, fungibleBalance: fungibleBalance) { |
||||
if let denialMessage = selection.denial { |
||||
return .warning(string: denialMessage) |
||||
} else { |
||||
//no-op shouldn't have reached here since the button should be disabled. So just do nothing to be safe |
||||
return .undefined |
||||
} |
||||
} else { |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
func buttonState(for action: TokenInstanceAction) -> ActionButtonState { |
||||
func _configButton(action: TokenInstanceAction) -> ActionButtonState { |
||||
let fungibleBalance = tokensService.tokenViewModel(for: token)?.balance.value |
||||
if let selection = action.activeExcludingSelection(selectedTokenHolders: [tokenHolder], forWalletAddress: wallet.address, fungibleBalance: fungibleBalance) { |
||||
if selection.denial == nil { |
||||
return .isDisplayed(false) |
||||
} |
||||
} |
||||
return .noOption |
||||
} |
||||
|
||||
switch wallet.type { |
||||
case .real: |
||||
return _configButton(action: action) |
||||
case .watch: |
||||
if session.config.development.shouldPretendIsRealWallet { |
||||
return _configButton(action: action) |
||||
} else { |
||||
return .isEnabled(false) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func markerCapViewModel(for ticker: CoinTicker?) -> TokenAttributeViewModel { |
||||
let value: String = ticker?.market_cap.flatMap { StringFormatter().largeNumberFormatter(for: $0, currency: "USD") } ?? "-" |
||||
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsMarket_cap(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private func totalSupplyViewModel(for ticker: CoinTicker?) -> TokenAttributeViewModel { |
||||
let value: String = ticker?.total_supply.flatMap { String($0) } ?? "-" |
||||
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsTotal_supply(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private func maxSupplyViewModel(for ticker: CoinTicker?) -> TokenAttributeViewModel { |
||||
let value: String = ticker?.max_supply.flatMap { Formatter.usd.string(from: $0) } ?? "-" |
||||
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsMax_supply(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var yearLowViewModel: TokenAttributeViewModel { |
||||
let value: String = { |
||||
let history = chartHistories[ChartHistoryPeriod.year] |
||||
if let min = HistoryHelper(history: history).minMax?.min, let value = Formatter.usd.string(from: min) { |
||||
return value |
||||
} else { |
||||
return "-" |
||||
} |
||||
}() |
||||
|
||||
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value) |
||||
return .init(title: R.string.localizable.tokenInfoFieldPerformanceYearLow(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var yearHighViewModel: TokenAttributeViewModel { |
||||
let value: String = { |
||||
let history = chartHistories[ChartHistoryPeriod.year] |
||||
if let max = HistoryHelper(history: history).minMax?.max, let value = Formatter.usd.string(from: max) { |
||||
return value |
||||
} else { |
||||
return "-" |
||||
} |
||||
}() |
||||
|
||||
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value) |
||||
return .init(title: R.string.localizable.tokenInfoFieldPerformanceYearHigh(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var yearViewModel: TokenAttributeViewModel { |
||||
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.year) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsYear(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var monthViewModel: TokenAttributeViewModel { |
||||
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.month) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsMonth(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var weekViewModel: TokenAttributeViewModel { |
||||
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.week) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsWeek(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var dayViewModel: TokenAttributeViewModel { |
||||
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.day) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsDay(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private func attributedHistoryValue(period: ChartHistoryPeriod) -> NSAttributedString { |
||||
let result: (string: String, foregroundColor: UIColor) = { |
||||
let result = HistoryHelper(history: chartHistories[period]) |
||||
|
||||
switch result.change { |
||||
case .appreciate(let percentage, let value): |
||||
let p = Formatter.percent.string(from: percentage) ?? "-" |
||||
let v = Formatter.usd.string(from: value) ?? "-" |
||||
|
||||
return ("\(v) (\(p)%)", Style.value.appreciated) |
||||
case .depreciate(let percentage, let value): |
||||
let p = Formatter.percent.string(from: percentage) ?? "-" |
||||
let v = Formatter.usd.string(from: value) ?? "-" |
||||
|
||||
return ("\(v) (\(p)%)", Style.value.depreciated) |
||||
case .none: |
||||
return ("-", Colors.black) |
||||
} |
||||
}() |
||||
|
||||
return TokenAttributeViewModel.attributedString(result.string, alignment: .right, font: Fonts.regular(size: 17), foregroundColor: result.foregroundColor, lineBreakMode: .byTruncatingTail) |
||||
} |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Screen.TokenCard.Color.background |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenDetailsViewModel { |
||||
enum TokenScriptWarningMessage { |
||||
case warning(string: String) |
||||
case undefined |
||||
} |
||||
|
||||
enum ActionButtonState { |
||||
case isDisplayed(Bool) |
||||
case isEnabled(Bool) |
||||
case noOption |
||||
} |
||||
|
||||
enum ViewType { |
||||
case charts |
||||
case testnet |
||||
case header(viewModel: TokenInfoHeaderViewModel) |
||||
case field(viewModel: TokenAttributeViewModel) |
||||
} |
||||
|
||||
struct ViewState { |
||||
let actions: [TokenInstanceAction] |
||||
let views: [FungibleTokenDetailsViewModel.ViewType] |
||||
} |
||||
} |
@ -0,0 +1,79 @@ |
||||
// |
||||
// FungibleTokenTabViewModel.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 20.11.2022. |
||||
// |
||||
|
||||
import UIKit |
||||
import AlphaWalletFoundation |
||||
import Combine |
||||
|
||||
struct FungibleTokenTabViewModelInput { |
||||
let willAppear: AnyPublisher<Void, Never> |
||||
} |
||||
|
||||
struct FungibleTokenTabViewModelOutput { |
||||
let viewState: AnyPublisher<FungibleTokenTabViewModel.ViewState, Never> |
||||
} |
||||
|
||||
class FungibleTokenTabViewModel { |
||||
private let token: Token |
||||
private let tokensService: TokenBalanceRefreshable & TokenViewModelState |
||||
private let assetDefinitionStore: AssetDefinitionStore |
||||
private var cancelable = Set<AnyCancellable>() |
||||
lazy var tokenScriptFileStatusHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore) |
||||
|
||||
let session: WalletSession |
||||
let tabBarItems: [TabBarItem] |
||||
|
||||
init(token: Token, session: WalletSession, tokensService: TokenBalanceRefreshable & TokenViewModelState, assetDefinitionStore: AssetDefinitionStore) { |
||||
self.tokensService = tokensService |
||||
self.assetDefinitionStore = assetDefinitionStore |
||||
self.token = token |
||||
self.session = session |
||||
|
||||
let hasTicker = tokensService.tokenViewModel(for: token)?.balance.ticker != nil |
||||
|
||||
if Features.default.isAvailable(.isAlertsEnabled) && hasTicker { |
||||
tabBarItems = [.details, .activities, .alerts] |
||||
} else { |
||||
tabBarItems = [.details, .activities] |
||||
} |
||||
} |
||||
|
||||
func transform(input: FungibleTokenTabViewModelInput) -> FungibleTokenTabViewModelOutput { |
||||
input.willAppear |
||||
.sink { [tokensService, token] _ in |
||||
tokensService.refreshBalance(updatePolicy: .token(token: token)) |
||||
}.store(in: &cancelable) |
||||
|
||||
let viewState = tokensService |
||||
.tokenViewModelPublisher(for: token) |
||||
.map { $0?.tokenScriptOverrides?.titleInPluralForm } |
||||
.map { FungibleTokenTabViewModel.ViewState(title: $0) } |
||||
.eraseToAnyPublisher() |
||||
|
||||
return .init(viewState: viewState) |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenTabViewModel { |
||||
enum TabBarItem: CustomStringConvertible { |
||||
case details |
||||
case activities |
||||
case alerts |
||||
|
||||
var description: String { |
||||
switch self { |
||||
case .details: return R.string.localizable.tokenTabInfo() |
||||
case .activities: return R.string.localizable.tokenTabActivity() |
||||
case .alerts: return R.string.localizable.priceAlertNavigationTitle() |
||||
} |
||||
} |
||||
} |
||||
|
||||
struct ViewState { |
||||
let title: String? |
||||
} |
||||
} |
@ -1,208 +0,0 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
import BigInt |
||||
import PromiseKit |
||||
import Combine |
||||
import AlphaWalletFoundation |
||||
|
||||
struct FungibleTokenViewModelInput { |
||||
let appear: AnyPublisher<Void, Never> |
||||
let updateAlert: AnyPublisher<(value: Bool, indexPath: IndexPath), Never> |
||||
let removeAlert: AnyPublisher<IndexPath, Never> |
||||
} |
||||
|
||||
struct FungibleTokenViewModelOutput { |
||||
let viewState: AnyPublisher<FungibleTokenViewModel.ViewState, Never> |
||||
let activities: AnyPublisher<ActivityPageViewModel, Never> |
||||
let alerts: AnyPublisher<PriceAlertsPageViewModel, Never> |
||||
} |
||||
|
||||
final class FungibleTokenViewModel { |
||||
private var cancelable = Set<AnyCancellable>() |
||||
private let coinTickersFetcher: CoinTickersFetcher |
||||
private let tokenActionsProvider: SupportedTokenActionsProvider |
||||
private let tokensService: TokenViewModelState & TokenBalanceRefreshable |
||||
private let activitiesService: ActivitiesServiceType |
||||
private let alertService: PriceAlertServiceType |
||||
private lazy var tokenHolder: TokenHolder = token.getTokenHolder(assetDefinitionStore: assetDefinitionStore, forWallet: session.account) |
||||
private (set) var actions: [TokenInstanceAction] = [] |
||||
|
||||
let session: WalletSession |
||||
let assetDefinitionStore: AssetDefinitionStore |
||||
var wallet: Wallet { session.account } |
||||
lazy var tokenScriptFileStatusHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore) |
||||
let token: Token |
||||
|
||||
var tokenScriptStatus: Promise<TokenLevelTokenScriptDisplayStatus> { |
||||
let xmlHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore) |
||||
return xmlHandler.tokenScriptStatus |
||||
} |
||||
|
||||
var hasCoinTicker: Bool { |
||||
return tokensService.tokenViewModel(for: token)?.balance.ticker != nil |
||||
} |
||||
|
||||
lazy var tokenInfoPageViewModel = TokenInfoPageViewModel(token: token, coinTickersFetcher: coinTickersFetcher, tokensService: tokensService) |
||||
|
||||
let backgroundColor: UIColor = Colors.appBackground |
||||
let sendButtonTitle: String = R.string.localizable.send() |
||||
let receiveButtonTitle: String = R.string.localizable.receive() |
||||
|
||||
init(activitiesService: ActivitiesServiceType, alertService: PriceAlertServiceType, token: Token, session: WalletSession, assetDefinitionStore: AssetDefinitionStore, tokenActionsProvider: SupportedTokenActionsProvider, coinTickersFetcher: CoinTickersFetcher, tokensService: TokenViewModelState & TokenBalanceRefreshable) { |
||||
self.activitiesService = activitiesService |
||||
self.alertService = alertService |
||||
self.token = token |
||||
self.session = session |
||||
self.assetDefinitionStore = assetDefinitionStore |
||||
self.tokenActionsProvider = tokenActionsProvider |
||||
self.coinTickersFetcher = coinTickersFetcher |
||||
self.tokensService = tokensService |
||||
} |
||||
|
||||
func tokenScriptWarningMessage(for action: TokenInstanceAction) -> TokenScriptWarningMessage? { |
||||
let fungibleBalance = tokensService.tokenViewModel(for: token)?.balance.value |
||||
if let selection = action.activeExcludingSelection(selectedTokenHolders: [tokenHolder], forWalletAddress: wallet.address, fungibleBalance: fungibleBalance) { |
||||
if let denialMessage = selection.denial { |
||||
return .warning(string: denialMessage) |
||||
} else { |
||||
//no-op shouldn't have reached here since the button should be disabled. So just do nothing to be safe |
||||
return .undefined |
||||
} |
||||
} else { |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
func buttonState(for action: TokenInstanceAction) -> ActionButtonState { |
||||
func _configButton(action: TokenInstanceAction) -> ActionButtonState { |
||||
let fungibleBalance = tokensService.tokenViewModel(for: token)?.balance.value |
||||
if let selection = action.activeExcludingSelection(selectedTokenHolders: [tokenHolder], forWalletAddress: wallet.address, fungibleBalance: fungibleBalance) { |
||||
if selection.denial == nil { |
||||
return .isDisplayed(false) |
||||
} |
||||
} |
||||
return .noOption |
||||
} |
||||
|
||||
switch wallet.type { |
||||
case .real: |
||||
return _configButton(action: action) |
||||
case .watch: |
||||
if session.config.development.shouldPretendIsRealWallet { |
||||
return _configButton(action: action) |
||||
} else { |
||||
return .isEnabled(false) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func transform(input: FungibleTokenViewModelInput) -> FungibleTokenViewModelOutput { |
||||
input.appear.receive(on: RunLoop.main) |
||||
.sink { [tokensService, token] _ in tokensService.refreshBalance(updatePolicy: .token(token: token)) } |
||||
.store(in: &cancelable) |
||||
|
||||
input.removeAlert |
||||
.sink { [alertService] in alertService.remove(indexPath: $0) } |
||||
.store(in: &cancelable) |
||||
|
||||
input.updateAlert |
||||
.sink { [alertService] in alertService.update(indexPath: $0.indexPath, update: .enabled($0.value)) } |
||||
.store(in: &cancelable) |
||||
|
||||
activitiesService.start() |
||||
|
||||
let whenTokenHolderHasChanged = tokenHolder.objectWillChange |
||||
.map { [tokensService, token] _ in tokensService.tokenViewModel(for: token) } |
||||
.receive(on: RunLoop.main) |
||||
.eraseToAnyPublisher() |
||||
|
||||
let whenTokenActionsHasChanged = tokenActionsProvider.objectWillChange |
||||
.map { [tokensService, token] _ in tokensService.tokenViewModel(for: token) } |
||||
.receive(on: RunLoop.main) |
||||
.eraseToAnyPublisher() |
||||
|
||||
let tokenViewModel = tokensService.tokenViewModelPublisher(for: token) |
||||
|
||||
let actions = Publishers.MergeMany(tokenViewModel, whenTokenHolderHasChanged, whenTokenActionsHasChanged) |
||||
.compactMap { _ in self.buildTokenActions() } |
||||
.handleEvents(receiveOutput: { self.actions = $0 }) |
||||
|
||||
let title = tokenViewModel.compactMap { $0?.tokenScriptOverrides?.titleInPluralForm } |
||||
|
||||
let activities = activitiesService.activitiesPublisher |
||||
.map { ActivityPageViewModel(activitiesViewModel: .init(collection: .init(activities: $0))) } |
||||
.receive(on: RunLoop.main) |
||||
|
||||
let alerts = alertService.alertsPublisher(forStrategy: .token(token)) |
||||
.map { PriceAlertsPageViewModel(alerts: $0) } |
||||
.receive(on: RunLoop.main) |
||||
|
||||
let viewState = Publishers.CombineLatest(actions, title) |
||||
.map { actions, title in FungibleTokenViewModel.ViewState(title: title, actions: actions) } |
||||
|
||||
return .init(viewState: viewState.eraseToAnyPublisher(), |
||||
activities: activities.eraseToAnyPublisher(), |
||||
alerts: alerts.eraseToAnyPublisher()) |
||||
} |
||||
|
||||
private func buildTokenActions() -> [TokenInstanceAction] { |
||||
let xmlHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore) |
||||
let actionsFromTokenScript = xmlHandler.actions |
||||
infoLog("[TokenScript] actions names: \(actionsFromTokenScript.map(\.name))") |
||||
if actionsFromTokenScript.isEmpty { |
||||
switch token.type { |
||||
case .erc875, .erc721, .erc721ForTickets, .erc1155: |
||||
return [] |
||||
case .erc20, .nativeCryptocurrency: |
||||
let actions: [TokenInstanceAction] = [ |
||||
.init(type: .erc20Send), |
||||
.init(type: .erc20Receive) |
||||
] |
||||
|
||||
return actions + tokenActionsProvider.actions(token: token) |
||||
} |
||||
} else { |
||||
switch token.type { |
||||
case .erc875, .erc721, .erc721ForTickets, .erc1155: |
||||
return [] |
||||
case .erc20: |
||||
return actionsFromTokenScript + tokenActionsProvider.actions(token: token) |
||||
case .nativeCryptocurrency: |
||||
//TODO we should support retrieval of XML (and XMLHandler) based on address + server. For now, this is only important for native cryptocurrency. So might be ok to check like this for now |
||||
if let server = xmlHandler.server, server.matches(server: token.server) { |
||||
return actionsFromTokenScript + tokenActionsProvider.actions(token: token) |
||||
} else { |
||||
//TODO .erc20Send and .erc20Receive names aren't appropriate |
||||
let actions: [TokenInstanceAction] = [ |
||||
.init(type: .erc20Send), |
||||
.init(type: .erc20Receive) |
||||
] |
||||
|
||||
return actions + tokenActionsProvider.actions(token: token) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
extension FungibleTokenViewModel { |
||||
|
||||
enum TokenScriptWarningMessage { |
||||
case warning(string: String) |
||||
case undefined |
||||
} |
||||
|
||||
enum ActionButtonState { |
||||
case isDisplayed(Bool) |
||||
case isEnabled(Bool) |
||||
case noOption |
||||
} |
||||
|
||||
struct ViewState { |
||||
let title: String |
||||
let actions: [TokenInstanceAction] |
||||
} |
||||
|
||||
} |
@ -1,188 +0,0 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
import Combine |
||||
import AlphaWalletFoundation |
||||
|
||||
struct TokenInfoPageViewModelInput { |
||||
let appear: AnyPublisher<Void, Never> |
||||
} |
||||
|
||||
struct TokenInfoPageViewModelOutput { |
||||
let viewState: AnyPublisher<TokenInfoPageViewModel.ViewState, Never> |
||||
} |
||||
|
||||
final class TokenInfoPageViewModel { |
||||
private var chartHistoriesSubject: CurrentValueSubject<[ChartHistoryPeriod: ChartHistory], Never> = .init([:]) |
||||
private let coinTickersFetcher: CoinTickersFetcher |
||||
private var ticker: CoinTicker? |
||||
private let tokensService: TokenViewModelState |
||||
private var cancelable = Set<AnyCancellable>() |
||||
private var chartHistories: [ChartHistoryPeriod: ChartHistory] { chartHistoriesSubject.value } |
||||
private lazy var coinTicker: AnyPublisher<CoinTicker?, Never> = { |
||||
return tokensService.tokenViewModelPublisher(for: token) |
||||
.map { $0?.balance.ticker } |
||||
.eraseToAnyPublisher() |
||||
}() |
||||
|
||||
var tabTitle: String { return R.string.localizable.tokenTabInfo() } |
||||
let token: Token |
||||
lazy var chartViewModel: TokenHistoryChartViewModel = .init(chartHistories: chartHistoriesSubject.eraseToAnyPublisher(), coinTicker: coinTicker) |
||||
lazy var headerViewModel: FungibleTokenHeaderViewModel = .init(token: token, tokensService: tokensService) |
||||
|
||||
init(token: Token, coinTickersFetcher: CoinTickersFetcher, tokensService: TokenViewModelState) { |
||||
self.tokensService = tokensService |
||||
self.coinTickersFetcher = coinTickersFetcher |
||||
self.token = token |
||||
} |
||||
|
||||
func transform(input: TokenInfoPageViewModelInput) -> TokenInfoPageViewModelOutput { |
||||
input.appear.flatMapLatest { [coinTickersFetcher, token] _ in |
||||
coinTickersFetcher.fetchChartHistories(for: .init(token: token), force: false, periods: ChartHistoryPeriod.allCases) |
||||
}.assign(to: \.value, on: chartHistoriesSubject) |
||||
.store(in: &cancelable) |
||||
|
||||
let coinTicker = coinTicker.handleEvents(receiveOutput: { [weak self] in self?.ticker = $0 }).map { _ in } |
||||
let chartHistories = chartHistoriesSubject.map { _ in } |
||||
let viewTypes = Publishers.Merge(coinTicker, chartHistories) |
||||
.compactMap { [weak self] _ in self?.buildViewTypes() } |
||||
|
||||
let viewState = viewTypes |
||||
.map { TokenInfoPageViewModel.ViewState(views: $0) } |
||||
.eraseToAnyPublisher() |
||||
|
||||
return .init(viewState: viewState) |
||||
} |
||||
|
||||
private func buildViewTypes() -> [TokenInfoPageViewModel.ViewType] { |
||||
var views: [TokenInfoPageViewModel.ViewType] = [] |
||||
|
||||
if token.server.isTestnet { |
||||
views = [ |
||||
.testnet |
||||
] |
||||
} else { |
||||
views = [ |
||||
.charts, |
||||
.header(viewModel: .init(title: R.string.localizable.tokenInfoHeaderPerformance())), |
||||
.field(viewModel: dayViewModel), |
||||
.field(viewModel: weekViewModel), |
||||
.field(viewModel: monthViewModel), |
||||
.field(viewModel: yearViewModel), |
||||
|
||||
.header(viewModel: .init(title: R.string.localizable.tokenInfoHeaderStats())), |
||||
.field(viewModel: markerCapViewModel), |
||||
.field(viewModel: yearLowViewModel), |
||||
.field(viewModel: yearHighViewModel) |
||||
] |
||||
} |
||||
|
||||
return views |
||||
} |
||||
|
||||
private var markerCapViewModel: TokenAttributeViewModel { |
||||
let value: String = ticker?.market_cap.flatMap { StringFormatter().largeNumberFormatter(for: $0, currency: "USD") } ?? "-" |
||||
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsMarket_cap(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var totalSupplyViewModel: TokenAttributeViewModel { |
||||
let value: String = ticker?.total_supply.flatMap { String($0) } ?? "-" |
||||
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsTotal_supply(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var maxSupplyViewModel: TokenAttributeViewModel { |
||||
let value: String = ticker?.max_supply.flatMap { Formatter.usd.string(from: $0) } ?? "-" |
||||
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsMax_supply(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var yearLowViewModel: TokenAttributeViewModel { |
||||
let value: String = { |
||||
let history = chartHistories[ChartHistoryPeriod.year] |
||||
if let min = HistoryHelper(history: history).minMax?.min, let value = Formatter.usd.string(from: min) { |
||||
return value |
||||
} else { |
||||
return "-" |
||||
} |
||||
}() |
||||
|
||||
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value) |
||||
return .init(title: R.string.localizable.tokenInfoFieldPerformanceYearLow(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var yearHighViewModel: TokenAttributeViewModel { |
||||
let value: String = { |
||||
let history = chartHistories[ChartHistoryPeriod.year] |
||||
if let max = HistoryHelper(history: history).minMax?.max, let value = Formatter.usd.string(from: max) { |
||||
return value |
||||
} else { |
||||
return "-" |
||||
} |
||||
}() |
||||
|
||||
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value) |
||||
return .init(title: R.string.localizable.tokenInfoFieldPerformanceYearHigh(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var yearViewModel: TokenAttributeViewModel { |
||||
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.year) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsYear(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var monthViewModel: TokenAttributeViewModel { |
||||
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.month) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsMonth(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var weekViewModel: TokenAttributeViewModel { |
||||
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.week) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsWeek(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private var dayViewModel: TokenAttributeViewModel { |
||||
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.day) |
||||
return .init(title: R.string.localizable.tokenInfoFieldStatsDay(), attributedValue: attributedValue) |
||||
} |
||||
|
||||
private func attributedHistoryValue(period: ChartHistoryPeriod) -> NSAttributedString { |
||||
let result: (string: String, foregroundColor: UIColor) = { |
||||
let result = HistoryHelper(history: chartHistories[period]) |
||||
|
||||
switch result.change { |
||||
case .appreciate(let percentage, let value): |
||||
let p = Formatter.percent.string(from: percentage) ?? "-" |
||||
let v = Formatter.usd.string(from: value) ?? "-" |
||||
|
||||
return ("\(v) (\(p)%)", Style.value.appreciated) |
||||
case .depreciate(let percentage, let value): |
||||
let p = Formatter.percent.string(from: percentage) ?? "-" |
||||
let v = Formatter.usd.string(from: value) ?? "-" |
||||
|
||||
return ("\(v) (\(p)%)", Style.value.depreciated) |
||||
case .none: |
||||
return ("-", Colors.black) |
||||
} |
||||
}() |
||||
|
||||
return TokenAttributeViewModel.attributedString(result.string, alignment: .right, font: Fonts.regular(size: 17), foregroundColor: result.foregroundColor, lineBreakMode: .byTruncatingTail) |
||||
} |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Screen.TokenCard.Color.background |
||||
} |
||||
} |
||||
|
||||
extension TokenInfoPageViewModel { |
||||
enum ViewType { |
||||
case charts |
||||
case testnet |
||||
case header(viewModel: TokenInfoHeaderViewModel) |
||||
case field(viewModel: TokenAttributeViewModel) |
||||
} |
||||
|
||||
struct ViewState { |
||||
let views: [TokenInfoPageViewModel.ViewType] |
||||
} |
||||
} |
Loading…
Reference in new issue