parent
15f2756265
commit
1ace5e3225
@ -1,176 +0,0 @@ |
||||
// Copyright © 2020 Stormbird PTE. LTD. |
||||
|
||||
import UIKit |
||||
|
||||
protocol SegmentedControlDelegate: AnyObject { |
||||
//Implementations of this protocol function will have to cast `segment` to the appropriate type. Maybe some generic or associated type magic can fix this, but alas time constraints |
||||
func didTapSegment(atSelection selection: SegmentedControl.Selection, inSegmentedControl segmentedControl: SegmentedControl) |
||||
} |
||||
|
||||
extension SegmentedControl { |
||||
static func tokensSegmentControl(titles: [String]) -> SegmentedControl { |
||||
let isNarrowScreen = ScreenChecker().isNarrowScreen |
||||
let spacing: CGFloat = isNarrowScreen ? 30 : 40 |
||||
let inset: CGFloat = isNarrowScreen ? 7 : 20 |
||||
|
||||
return .init(titles: titles, segmentConfiguration: .init(spacing: spacing, selectionIndicatorInsets: .init(top: 0, left: inset, bottom: 0, right: inset), selectionBarHeight: 3, barHeight: 1)) |
||||
} |
||||
} |
||||
|
||||
class SegmentedControl: UIView, ReusableTableHeaderViewType { |
||||
enum Alignment { |
||||
case left |
||||
case right |
||||
case center |
||||
} |
||||
|
||||
enum Selection: Equatable { |
||||
case selected(UInt) |
||||
case unselected |
||||
} |
||||
|
||||
private let buttons: [UIButton] |
||||
private let highlightedBar = UIView() |
||||
private var highlightBarHorizontalConstraints: [NSLayoutConstraint]? |
||||
private lazy var viewModel = SegmentedControlViewModel(selection: selection) |
||||
|
||||
weak var delegate: SegmentedControlDelegate? |
||||
var selection: Selection = .selected(0) { |
||||
didSet { |
||||
if oldValue == selection { return } |
||||
viewModel.selection = selection |
||||
configureTitleButtons() |
||||
configureHighlightedBar() |
||||
} |
||||
} |
||||
|
||||
struct SegmentConfiguration { |
||||
var spacing: CGFloat = 20 |
||||
var selectionIndicatorInsets: UIEdgeInsets = .init(top: 0, left: 7, bottom: 0, right: 7) |
||||
var selectionBarHeight: CGFloat = 3 |
||||
var barHeight: CGFloat = 1 |
||||
} |
||||
|
||||
private let segmentConfiguration: SegmentConfiguration |
||||
|
||||
init(titles: [String], alignment: Alignment = .left, distribution: UIStackView.Distribution = .fill, segmentConfiguration: SegmentConfiguration = .init()) { |
||||
self.buttons = SegmentedControl.createButtons(fromTitles: titles) |
||||
self.segmentConfiguration = segmentConfiguration |
||||
super.init(frame: .zero) |
||||
|
||||
backgroundColor = viewModel.backgroundColor |
||||
|
||||
for each in buttons { |
||||
each.addTarget(self, action: #selector(segmentTapped), for: .touchUpInside) |
||||
} |
||||
let buttonsStackView = buttons.map { $0 as UIView }.asStackView(distribution: distribution, spacing: segmentConfiguration.spacing) |
||||
buttonsStackView.translatesAutoresizingMaskIntoConstraints = false |
||||
addSubview(buttonsStackView) |
||||
|
||||
let fullWidthBar = UIView() |
||||
fullWidthBar.translatesAutoresizingMaskIntoConstraints = false |
||||
fullWidthBar.backgroundColor = viewModel.unselectedBarColor |
||||
addSubview(fullWidthBar) |
||||
|
||||
highlightedBar.translatesAutoresizingMaskIntoConstraints = false |
||||
fullWidthBar.addSubview(highlightedBar) |
||||
|
||||
let barHeightConstraint = fullWidthBar.heightAnchor.constraint(equalToConstant: segmentConfiguration.barHeight) |
||||
barHeightConstraint.priority = .defaultHigh |
||||
|
||||
var contraints: [NSLayoutConstraint] = [] |
||||
|
||||
switch alignment { |
||||
case .left: |
||||
let stackViewLeadingConstraint = buttonsStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 17) |
||||
stackViewLeadingConstraint.priority = .defaultHigh |
||||
|
||||
let stackViewWidthConstraint = buttonsStackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, constant: -17) |
||||
stackViewWidthConstraint.priority = .defaultHigh |
||||
|
||||
contraints = [stackViewLeadingConstraint, stackViewWidthConstraint] |
||||
case .center: |
||||
let stackViewCenterConstraint = buttonsStackView.centerXAnchor.constraint(equalTo: centerXAnchor) |
||||
stackViewCenterConstraint.priority = .defaultHigh |
||||
|
||||
let stackViewWidthConstraint = buttonsStackView.widthAnchor.constraint(equalTo: widthAnchor, constant: -34) |
||||
stackViewWidthConstraint.priority = .defaultHigh |
||||
|
||||
contraints = [stackViewCenterConstraint, stackViewWidthConstraint] |
||||
case .right: |
||||
let stackViewLeadingConstraint = buttonsStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -17) |
||||
stackViewLeadingConstraint.priority = .defaultHigh |
||||
|
||||
let stackViewWidthConstraint = buttonsStackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, constant: -17) |
||||
stackViewWidthConstraint.priority = .defaultHigh |
||||
|
||||
contraints = [stackViewLeadingConstraint, stackViewWidthConstraint] |
||||
} |
||||
|
||||
NSLayoutConstraint.activate(contraints + [ |
||||
buttonsStackView.topAnchor.constraint(equalTo: topAnchor), |
||||
buttonsStackView.bottomAnchor.constraint(equalTo: fullWidthBar.topAnchor), |
||||
|
||||
fullWidthBar.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||
fullWidthBar.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||
barHeightConstraint, |
||||
fullWidthBar.bottomAnchor.constraint(equalTo: bottomAnchor), |
||||
|
||||
highlightedBar.heightAnchor.constraint(equalToConstant: segmentConfiguration.selectionBarHeight), |
||||
highlightedBar.bottomAnchor.constraint(equalTo: fullWidthBar.bottomAnchor), |
||||
]) |
||||
|
||||
configureTitleButtons() |
||||
configureHighlightedBar() |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
return nil |
||||
} |
||||
|
||||
private static func createButtons(fromTitles titles: [String]) -> [UIButton] { |
||||
return titles.map { |
||||
let button = UIButton(type: .system) |
||||
button.setTitle($0, for: .normal) |
||||
return button |
||||
} |
||||
} |
||||
|
||||
@objc private func segmentTapped(_ source: UIButton) { |
||||
guard let segment = buttons.firstIndex(of: source).flatMap({ UInt($0) }) else { return } |
||||
delegate?.didTapSegment(atSelection: .selected(segment), inSegmentedControl: self) |
||||
} |
||||
|
||||
func configureTitleButtons() { |
||||
for (index, each) in buttons.enumerated() { |
||||
//This is safe only because index can't possibly be negative |
||||
let index = UInt(index) |
||||
each.setTitleColor(viewModel.titleColor(forSelection: .selected(index)), for: .normal) |
||||
each.titleLabel?.font = viewModel.titleFont(forSelection: .selected(index)) |
||||
} |
||||
} |
||||
|
||||
func configureHighlightedBar() { |
||||
switch selection { |
||||
case .selected(let index): |
||||
highlightedBar.backgroundColor = viewModel.selectedBarColor |
||||
let index = Int(index) |
||||
let button: UIButton = buttons[index] |
||||
if let previousConstraints = highlightBarHorizontalConstraints { |
||||
NSLayoutConstraint.deactivate(previousConstraints) |
||||
} |
||||
highlightBarHorizontalConstraints = [ |
||||
highlightedBar.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: -segmentConfiguration.selectionIndicatorInsets.left), |
||||
highlightedBar.trailingAnchor.constraint(equalTo: button.trailingAnchor, constant: segmentConfiguration.selectionIndicatorInsets.right), |
||||
] |
||||
if let constraints = highlightBarHorizontalConstraints { |
||||
NSLayoutConstraint.activate(constraints) |
||||
} |
||||
UIView.animate(withDuration: 0.7, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 10, options: .allowUserInteraction, animations: { |
||||
self.layoutIfNeeded() |
||||
}) |
||||
case .unselected: |
||||
highlightedBar.backgroundColor = nil |
||||
} |
||||
} |
||||
} |
@ -1,244 +0,0 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import Alamofire |
||||
import BigInt |
||||
import PromiseKit |
||||
import Result |
||||
import SwiftyJSON |
||||
|
||||
class OpenSea { |
||||
typealias PromiseResult = Promise<[AlphaWallet.Address: [OpenSeaNonFungible]]> |
||||
|
||||
//Assuming 1 token (token ID, rather than a token) is 4kb, 1500 HyperDragons is 6MB. So we rate limit requests |
||||
private static let numberOfTokenIdsBeforeRateLimitingRequests = 25 |
||||
private static let minimumSecondsBetweenRequests = TimeInterval(60) |
||||
private static let dateFormatter: DateFormatter = { |
||||
//Expect date string from asset_contract/created_date, etc as: "2020-05-27T16:53:32.834583" |
||||
let dateFormatter = DateFormatter() |
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" |
||||
return dateFormatter |
||||
}() |
||||
private static var instances = [AddressAndRPCServer: WeakRef<OpenSea>]() |
||||
//NOTE: using AddressAndRPCServer fixes issue with incorrect tokens returned from makeFetchPromise |
||||
// the problem was that cached OpenSea returned tokens from multiple wallets |
||||
private let key: AddressAndRPCServer |
||||
private var recentWalletsWithManyTokens = [AlphaWallet.Address: (Date, PromiseResult)]() |
||||
private var fetch = OpenSea.makeEmptyFulfilledPromise() |
||||
private let queue = DispatchQueue.global(qos: .userInitiated) |
||||
|
||||
private init(key: AddressAndRPCServer) { |
||||
self.key = key |
||||
} |
||||
|
||||
static func createInstance(with key: AddressAndRPCServer) -> OpenSea { |
||||
if let instance = instances[key]?.object { |
||||
return instance |
||||
} else { |
||||
let instance = OpenSea(key: key) |
||||
instances[key] = WeakRef(object: instance) |
||||
return instance |
||||
} |
||||
} |
||||
|
||||
private static func makeEmptyFulfilledPromise() -> PromiseResult { |
||||
return Promise { |
||||
$0.fulfill([:]) |
||||
} |
||||
} |
||||
|
||||
static func isServerSupported(_ server: RPCServer) -> Bool { |
||||
switch server { |
||||
case .main, .rinkeby: |
||||
return true |
||||
case .kovan, .ropsten, .poa, .sokol, .classic, .callisto, .custom, .goerli, .xDai, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .heco, .heco_testnet, .fantom, .fantom_testnet, .avalanche, .avalanche_testnet, .polygon, .mumbai_testnet, .optimistic, .optimisticKovan, .cronosTestnet, .arbitrum, .arbitrumRinkeby, .palm, .palmTestnet: |
||||
return false |
||||
} |
||||
} |
||||
|
||||
static func resetInstances() { |
||||
for each in instances.values { |
||||
each.object?.reset() |
||||
} |
||||
} |
||||
|
||||
///Call this after switching wallets, otherwise when the current promise is fulfilled, the switched to wallet will think the API results are for them |
||||
private func reset() { |
||||
fetch = OpenSea.makeEmptyFulfilledPromise() |
||||
} |
||||
|
||||
///Uses a promise to make sure we don't fetch from OpenSea multiple times concurrently |
||||
func makeFetchPromise() -> PromiseResult { |
||||
guard OpenSea.isServerSupported(key.server) else { |
||||
fetch = .value([:]) |
||||
return fetch |
||||
} |
||||
let owner = key.address |
||||
trimCachedPromises() |
||||
if let cachedPromise = cachedPromise(forOwner: owner) { |
||||
return cachedPromise |
||||
} |
||||
|
||||
if fetch.isResolved { |
||||
fetch = Promise { seal in |
||||
let offset = 0 |
||||
fetchPage(forOwner: owner, offset: offset) { result in |
||||
switch result { |
||||
case .success(let result): |
||||
|
||||
seal.fulfill(result) |
||||
case .failure(let error): |
||||
verboseLog("[OpenSea] fetch failed: \(error) owner: \(owner.eip55String) offset: \(offset)") |
||||
seal.reject(error) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return fetch |
||||
} |
||||
|
||||
private static func getBaseURLForOpensea(for server: RPCServer) -> String { |
||||
switch server { |
||||
case .main: |
||||
return Constants.openseaAPI |
||||
case .rinkeby: |
||||
return Constants.openseaRinkebyAPI |
||||
case .kovan, .ropsten, .poa, .sokol, .classic, .callisto, .xDai, .goerli, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .custom, .heco, .heco_testnet, .fantom, .fantom_testnet, .avalanche, .avalanche_testnet, .polygon, .mumbai_testnet, .optimistic, .optimisticKovan, .cronosTestnet, .arbitrum, .arbitrumRinkeby, .palm, .palmTestnet: |
||||
return Constants.openseaAPI |
||||
} |
||||
} |
||||
|
||||
static func fetchAsset(for value: Eip155URL) -> Promise<URL> { |
||||
let baseURL = getBaseURLForOpensea(for: .main) |
||||
guard let url = URL(string: "\(baseURL)api/v1/asset/\(value.path)") else { |
||||
return .init(error: AnyError(OpenSeaError(localizedDescription: "Error calling \(baseURL) API isMainThread: \(Thread.isMainThread)"))) |
||||
} |
||||
|
||||
return Promise<URL> { seal in |
||||
Alamofire |
||||
.request(url, method: .get, headers: ["X-API-KEY": Constants.Credentials.openseaKey]) |
||||
.responseJSON(queue: .main, options: .allowFragments, completionHandler: { response in |
||||
guard let data = response.data, let json = try? JSON(data: data) else { |
||||
return seal.reject(AnyError(OpenSeaError(localizedDescription: "Error calling \(baseURL) API: \(String(describing: response.error))"))) |
||||
} |
||||
|
||||
let image: String = json["image_url"].string ?? json["image_preview_url"].string ?? json["image_thumbnail_url"].string ?? json["image_original_url"].string ?? "" |
||||
guard let url = URL(string: image) else { |
||||
return seal.reject(AnyError(OpenSeaError(localizedDescription: "Error calling \(baseURL) API: \(String(describing: response.error))"))) |
||||
} |
||||
seal.fulfill(url) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
private func fetchPage(forOwner owner: AlphaWallet.Address, offset: Int, sum: [AlphaWallet.Address: [OpenSeaNonFungible]] = [:], completion: @escaping (ResultResult<[AlphaWallet.Address: [OpenSeaNonFungible]], AnyError>.t) -> Void) { |
||||
let baseURL = Self.getBaseURLForOpensea(for: key.server) |
||||
//Careful to `order_by` with a valid value otherwise OpenSea will return 0 results |
||||
guard let url = URL(string: "\(baseURL)api/v1/assets/?owner=\(owner.eip55String)&order_by=pk&order_direction=asc&limit=50&offset=\(offset)") else { |
||||
completion(.failure(AnyError(OpenSeaError(localizedDescription: "Error calling \(baseURL) API isMainThread: \(Thread.isMainThread)")))) |
||||
return |
||||
} |
||||
|
||||
Alamofire.request( |
||||
url, |
||||
method: .get, |
||||
headers: ["X-API-KEY": Constants.Credentials.openseaKey] |
||||
).responseJSON(queue: queue, options: .allowFragments, completionHandler: { [weak self] response in |
||||
guard let strongSelf = self else { return } |
||||
guard let data = response.data, let json = try? JSON(data: data) else { |
||||
completion(.failure(AnyError(OpenSeaError(localizedDescription: "Error calling \(baseURL) API: \(String(describing: response.error))")))) |
||||
return |
||||
} |
||||
|
||||
var results = sum |
||||
for (_, each): (String, JSON) in json["assets"] { |
||||
let type = each["asset_contract"]["schema_name"].stringValue |
||||
guard let tokenType = NonFungibleFromJsonTokenType(rawString: type) else { continue } |
||||
if !Features.isErc1155Enabled && tokenType == .erc1155 { continue } |
||||
let tokenId = each["token_id"].stringValue |
||||
let contractName = each["asset_contract"]["name"].stringValue |
||||
//So if it's null in OpenSea, we get a 0, as expected. And 0 works for ERC721 too |
||||
let decimals = each["decimals"].intValue |
||||
let value: BigInt |
||||
switch tokenType { |
||||
case .erc721: |
||||
value = 1 |
||||
case .erc1155: |
||||
//OpenSea API doesn't include value for ERC1155, so we'll have to batch fetch it later for each contract before we update the database |
||||
value = 0 |
||||
} |
||||
let symbol = each["asset_contract"]["symbol"].stringValue |
||||
let name = each["name"].stringValue |
||||
let description = each["description"].stringValue |
||||
let thumbnailUrl = each["image_thumbnail_url"].stringValue |
||||
//We'll get what seems to be the PNG version first, falling back to the sometimes PNG, but sometimes SVG version |
||||
var imageUrl = each["image_preview_url"].stringValue |
||||
if imageUrl.isEmpty { |
||||
imageUrl = each["image_url"].stringValue |
||||
} |
||||
let contractImageUrl = each["asset_contract"]["image_url"].stringValue |
||||
let externalLink = each["external_link"].stringValue |
||||
let backgroundColor = each["background_color"].stringValue |
||||
var traits = [OpenSeaNonFungibleTrait]() |
||||
for each in each["traits"].arrayValue { |
||||
let traitCount = each["trait_count"].intValue |
||||
let traitType = each["trait_type"].stringValue |
||||
let traitValue = each["value"].stringValue |
||||
let trait = OpenSeaNonFungibleTrait(count: traitCount, type: traitType, value: traitValue) |
||||
traits.append(trait) |
||||
} |
||||
if let contract = AlphaWallet.Address(string: each["asset_contract"]["address"].stringValue) { |
||||
let collectionCreatedDate = each["asset_contract"]["created_date"].string.flatMap { OpenSea.dateFormatter.date(from: $0) } |
||||
let collectionDescription = each["asset_contract"]["description"].string |
||||
let cat = OpenSeaNonFungible(tokenId: tokenId, tokenType: tokenType, value: value, contractName: contractName, decimals: decimals, symbol: symbol, name: name, description: description, thumbnailUrl: thumbnailUrl, imageUrl: imageUrl, contractImageUrl: contractImageUrl, externalLink: externalLink, backgroundColor: backgroundColor, traits: traits, collectionCreatedDate: collectionCreatedDate, collectionDescription: collectionDescription) |
||||
if var list = results[contract] { |
||||
list.append(cat) |
||||
results[contract] = list |
||||
} else { |
||||
let list = [cat] |
||||
results[contract] = list |
||||
} |
||||
} |
||||
} |
||||
|
||||
let fetchedCount = json["assets"].count |
||||
verboseLog("[OpenSea] fetch page count: \(fetchedCount) owner: \(owner.eip55String) offset: \(offset)") |
||||
if fetchedCount > 0 { |
||||
strongSelf.fetchPage(forOwner: owner, offset: offset + fetchedCount, sum: results) { results in |
||||
completion(results) |
||||
} |
||||
} else { |
||||
//Ignore UEFA from OpenSea, otherwise the token type would be saved wrongly as `.erc721` instead of `.erc721ForTickets` |
||||
let excludingUefa = sum.filter { !$0.key.isUEFATicketContract } |
||||
var tokenIdCount = 0 |
||||
for (_, tokenIds) in excludingUefa { |
||||
tokenIdCount += tokenIds.count |
||||
} |
||||
strongSelf.cachePromise(withTokenIdCount: tokenIdCount, forOwner: owner) |
||||
|
||||
completion(.success(excludingUefa)) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
private func cachePromise(withTokenIdCount tokenIdCount: Int, forOwner wallet: AlphaWallet.Address) { |
||||
guard tokenIdCount >= OpenSea.numberOfTokenIdsBeforeRateLimitingRequests else { return } |
||||
recentWalletsWithManyTokens[wallet] = (Date(), fetch) |
||||
} |
||||
|
||||
private func cachedPromise(forOwner wallet: AlphaWallet.Address) -> PromiseResult? { |
||||
guard let (_, promise) = recentWalletsWithManyTokens[wallet] else { return nil } |
||||
return promise |
||||
} |
||||
|
||||
private func trimCachedPromises() { |
||||
let cachedWallets = recentWalletsWithManyTokens.keys |
||||
let now = Date() |
||||
for each in cachedWallets { |
||||
guard let (date, _) = recentWalletsWithManyTokens[each] else { continue } |
||||
if now.timeIntervalSince(date) >= OpenSea.minimumSecondsBetweenRequests { |
||||
recentWalletsWithManyTokens.removeValue(forKey: each) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,84 +0,0 @@ |
||||
// |
||||
// TokensCardCollectionInfoPageViewModel.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 07.09.2021. |
||||
// |
||||
|
||||
import UIKit |
||||
|
||||
enum TokensCardCollectionInfoPageViewConfiguration { |
||||
case field(viewModel: TokenInstanceAttributeViewModel) |
||||
case header(viewModel: TokenInfoHeaderViewModel) |
||||
} |
||||
|
||||
struct TokensCardCollectionInfoPageViewModel { |
||||
|
||||
var tabTitle: String { |
||||
return R.string.localizable.tokenTabInfo() |
||||
} |
||||
|
||||
private let tokenObject: TokenObject |
||||
|
||||
let server: RPCServer |
||||
var contractAddress: AlphaWallet.Address { |
||||
tokenObject.contractAddress |
||||
} |
||||
let tokenHolders: [TokenHolder] |
||||
var configurations: [TokensCardCollectionInfoPageViewConfiguration] = [] |
||||
var image: URL? { |
||||
return tokenHolders.first.flatMap { $0.values.imageUrlUrlValue ?? $0.values.thumbnailUrlUrlValue } |
||||
} |
||||
|
||||
var tokenImagePlaceholder: UIImage? { |
||||
return R.image.tokenPlaceholderLarge() |
||||
} |
||||
|
||||
init(server: RPCServer, token: TokenObject, assetDefinitionStore: AssetDefinitionStore, eventsDataStore: EventsDataStoreProtocol, forWallet wallet: Wallet) { |
||||
self.server = server |
||||
self.tokenObject = token |
||||
tokenHolders = TokenAdaptor(token: token, assetDefinitionStore: assetDefinitionStore, eventsDataStore: eventsDataStore).getTokenHolders(forWallet: wallet) |
||||
configurations = generateConfigurations(token, tokenHolders: tokenHolders) |
||||
} |
||||
|
||||
var createdDateViewModel: TokenInstanceAttributeViewModel { |
||||
let string: String? = tokenHolders.first?.values.collectionCreatedDateGeneralisedTimeValue?.formatAsShortDateString() |
||||
let attributedString: NSAttributedString? = string.flatMap { TokenInstanceAttributeViewModel.defaultValueAttributedString($0) } |
||||
return .init(title: R.string.localizable.semifungiblesCreatedDate(), attributedValue: attributedString) |
||||
} |
||||
|
||||
var descriptionViewModel: TokenInstanceAttributeViewModel { |
||||
let string: String? = tokenHolders.first?.values.collectionDescriptionStringValue |
||||
let attributedString: NSAttributedString? = string.flatMap { TokenInstanceAttributeViewModel.defaultValueAttributedString($0, alignment: .left) } |
||||
return .init(title: nil, attributedValue: attributedString, isSeparatorHidden: true) |
||||
} |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Screen.TokenCard.Color.background |
||||
} |
||||
|
||||
var iconImage: Subscribable<TokenImage> { |
||||
tokenObject.icon |
||||
} |
||||
|
||||
var blockChainTagViewModel: BlockchainTagLabelViewModel { |
||||
.init(server: server) |
||||
} |
||||
|
||||
func generateConfigurations(_ tokenObject: TokenObject, tokenHolders: [TokenHolder]) -> [TokensCardCollectionInfoPageViewConfiguration] { |
||||
var configurations: [TokensCardCollectionInfoPageViewConfiguration] = [] |
||||
|
||||
configurations = [ |
||||
.header(viewModel: .init(title: R.string.localizable.semifungiblesDetails())), |
||||
.field(viewModel: createdDateViewModel), |
||||
] |
||||
|
||||
configurations += [ |
||||
.header(viewModel: .init(title: R.string.localizable.semifungiblesDescription())), |
||||
.field(viewModel: descriptionViewModel), |
||||
] |
||||
|
||||
return configurations |
||||
} |
||||
} |
||||
|
@ -1,29 +0,0 @@ |
||||
// |
||||
// SendViewSectionHeaderViewModel.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 01.06.2020. |
||||
// |
||||
|
||||
import UIKit |
||||
|
||||
struct SendViewSectionHeaderViewModel { |
||||
|
||||
let text: String |
||||
var showTopSeparatorLine: Bool = true |
||||
|
||||
var font: UIFont { |
||||
return Fonts.semibold(size: 15)! |
||||
} |
||||
var textColor: UIColor { |
||||
return R.color.dove()! |
||||
} |
||||
var backgroundColor: UIColor { |
||||
return R.color.alabaster()! |
||||
} |
||||
|
||||
var separatorBackgroundColor: UIColor { |
||||
return R.color.mike()! |
||||
} |
||||
} |
||||
|
@ -1,77 +0,0 @@ |
||||
// |
||||
// SendViewSectionHeader.swift |
||||
// AlphaWallet |
||||
// |
||||
// Created by Vladyslav Shepitko on 01.06.2020. |
||||
// |
||||
|
||||
import UIKit |
||||
|
||||
class SendViewSectionHeader: UIView { |
||||
|
||||
private let textLabel: UILabel = { |
||||
let label = UILabel() |
||||
label.translatesAutoresizingMaskIntoConstraints = false |
||||
label.numberOfLines = 0 |
||||
label.setContentHuggingPriority(.required, for: .vertical) |
||||
label.setContentCompressionResistancePriority(.required, for: .vertical) |
||||
|
||||
return label |
||||
}() |
||||
|
||||
private let topSeparatorView: UIView = { |
||||
let view = UIView() |
||||
view.translatesAutoresizingMaskIntoConstraints = false |
||||
return view |
||||
}() |
||||
|
||||
private let bottomSeparatorView: UIView = { |
||||
let view = UIView() |
||||
view.translatesAutoresizingMaskIntoConstraints = false |
||||
return view |
||||
}() |
||||
private var topSeparatorLineHeight: NSLayoutConstraint! |
||||
|
||||
init() { |
||||
super.init(frame: .zero) |
||||
setupView() |
||||
} |
||||
|
||||
required init?(coder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
private func setupView() { |
||||
translatesAutoresizingMaskIntoConstraints = false |
||||
|
||||
addSubview(topSeparatorView) |
||||
addSubview(textLabel) |
||||
addSubview(bottomSeparatorView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
textLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), |
||||
textLabel.trailingAnchor.constraint(greaterThanOrEqualTo: trailingAnchor, constant: -16), |
||||
textLabel.topAnchor.constraint(equalTo: topAnchor, constant: 13), |
||||
textLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -13), |
||||
|
||||
topSeparatorView.topAnchor.constraint(equalTo: topAnchor), |
||||
topSeparatorView.widthAnchor.constraint(equalTo: widthAnchor), |
||||
|
||||
bottomSeparatorView.topAnchor.constraint(equalTo: bottomAnchor), |
||||
bottomSeparatorView.widthAnchor.constraint(equalTo: widthAnchor), |
||||
bottomSeparatorView.heightAnchor.constraint(equalToConstant: 1) |
||||
]) |
||||
topSeparatorLineHeight = topSeparatorView.heightAnchor.constraint(equalToConstant: 1) |
||||
topSeparatorLineHeight.isActive = true |
||||
} |
||||
|
||||
func configure(viewModel: SendViewSectionHeaderViewModel) { |
||||
textLabel.text = viewModel.text |
||||
textLabel.textColor = viewModel.textColor |
||||
textLabel.font = viewModel.font |
||||
backgroundColor = viewModel.backgroundColor |
||||
topSeparatorView.backgroundColor = viewModel.separatorBackgroundColor |
||||
bottomSeparatorView.backgroundColor = viewModel.separatorBackgroundColor |
||||
topSeparatorLineHeight.constant = viewModel.showTopSeparatorLine ? 1 : 0 |
||||
} |
||||
} |
@ -1,33 +0,0 @@ |
||||
// |
||||
// AlphaWalletAddressTests.swift |
||||
// AlphaWalletAddressTests |
||||
// |
||||
// Created by Vladyslav Shepitko on 13.10.2021. |
||||
// |
||||
|
||||
import XCTest |
||||
@testable import AlphaWalletAddress |
||||
|
||||
class AlphaWalletAddressTests: XCTestCase { |
||||
|
||||
override func setUpWithError() throws { |
||||
// Put setup code here. This method is called before the invocation of each test method in the class. |
||||
} |
||||
|
||||
override func tearDownWithError() throws { |
||||
// Put teardown code here. This method is called after the invocation of each test method in the class. |
||||
} |
||||
|
||||
func testExample() throws { |
||||
// This is an example of a functional test case. |
||||
// Use XCTAssert and related functions to verify your tests produce the correct results. |
||||
} |
||||
|
||||
func testPerformanceExample() throws { |
||||
// This is an example of a performance test case. |
||||
self.measure { |
||||
// Put the code you want to measure the time of here. |
||||
} |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue