Add 3rd party sources for token icons — OpenSea (for ERC721) and a GitHub repo of images

pull/1933/head
Hwee-Boon Yar 4 years ago
parent ef1c1a0246
commit 0d5584ef2e
  1. 4
      AlphaWallet.xcodeproj/project.pbxproj
  2. 2
      AlphaWallet/EtherClient/OpenSea.swift
  3. 16
      AlphaWallet/Tokens/ViewModels/EthTokenViewCellViewModel.swift
  4. 16
      AlphaWallet/Tokens/ViewModels/FungibleTokenViewCellViewModel.swift
  5. 16
      AlphaWallet/Tokens/ViewModels/NonFungibleTokenViewCellViewModel.swift
  6. 19
      AlphaWallet/Tokens/Views/EthTokenViewCell.swift
  7. 19
      AlphaWallet/Tokens/Views/FungibleTokenViewCell.swift
  8. 19
      AlphaWallet/Tokens/Views/NonFungibleTokenViewCell.swift
  9. 30
      AlphaWallet/UI/SelectCurrencyButton.swift
  10. 62
      AlphaWallet/UI/TokenImageView.swift
  11. 117
      AlphaWallet/UI/TokenObject+UI.swift
  12. 18
      AlphaWallet/UI/Views/AmountTextField.swift

@ -335,6 +335,7 @@
5E7C74A655E5D34953FFEBF2 /* AlphaWalletAddressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7570889AD33EA942D9A6 /* AlphaWalletAddressTests.swift */; };
5E7C74B07DDDBE3344273CB7 /* PromptBackupWalletAfterWalletCreationViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C723C21F6376387AD1DCE /* PromptBackupWalletAfterWalletCreationViewViewModel.swift */; };
5E7C74B5796FB59C8427C7A0 /* GenerateTransferMagicLinkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D46C7CABC31A7477F37 /* GenerateTransferMagicLinkViewController.swift */; };
5E7C74B615A84B248D3BE76C /* TokenImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C67C1FF0BFDDA82C61E /* TokenImageView.swift */; };
5E7C74B99922D0CAB635970E /* PasscodeCharacterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B9220E616F82EDA956F /* PasscodeCharacterView.swift */; };
5E7C74BD08801CABF9695853 /* LocaleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79778E4BFE1322711EA6 /* LocaleViewModel.swift */; };
5E7C74C1C2AB84F9AFAC630E /* TokenCardRowViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C12E88EB0B73AA1E562 /* TokenCardRowViewModelProtocol.swift */; };
@ -1226,6 +1227,7 @@
5E7C7C5454600A70DCFD7C0E /* BoxView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxView.swift; sourceTree = "<group>"; };
5E7C7C58A99977A9D4BE0512 /* SendHeaderViewWithIntroduction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendHeaderViewWithIntroduction.swift; sourceTree = "<group>"; };
5E7C7C6759CA1C223DABA462 /* HDWallet+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HDWallet+Extension.swift"; sourceTree = "<group>"; };
5E7C7C67C1FF0BFDDA82C61E /* TokenImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenImageView.swift; sourceTree = "<group>"; };
5E7C7C781CCE43B6451671B9 /* TokenListFormatTableViewCellWithoutCheckbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenListFormatTableViewCellWithoutCheckbox.swift; sourceTree = "<group>"; };
5E7C7C7CB95B7EE4B2547585 /* EnabledServersCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledServersCoordinator.swift; sourceTree = "<group>"; };
5E7C7C83B57FC8FAE9AF8F26 /* TokenCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenCollection.swift; sourceTree = "<group>"; };
@ -1684,6 +1686,7 @@
5E7C76052512831B707659CA /* WhereIsWalletAddressFoundOverlayView.swift */,
5E7C78CDFEB86A8356EA5818 /* TokenCardRowViewProtocol.swift */,
5E7C74F1B6944E7BD06DD2C2 /* TokenObject+UI.swift */,
5E7C7C67C1FF0BFDDA82C61E /* TokenImageView.swift */,
);
path = UI;
sourceTree = "<group>";
@ -4511,6 +4514,7 @@
5E7C710D1522AFA533B0C22B /* XMLHandler+XPaths.swift in Sources */,
5E7C782069BAD4A667543979 /* SettingsViewController.swift in Sources */,
5E7C750721DA2745432B1857 /* TokenObject+UI.swift in Sources */,
5E7C74B615A84B248D3BE76C /* TokenImageView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

@ -136,7 +136,7 @@ class OpenSea {
if imageUrl.isEmpty {
imageUrl = each["image_url"].stringValue
}
let contractImageUrl = each["asset_contract"]["featured_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]()

@ -114,19 +114,7 @@ struct EthTokenViewCellViewModel {
return isVisible ? 1.0 : 0.4
}
var iconImage: UIImage {
token.icon.image
}
var symbolInIcon: String {
token.icon.symbol
}
var symbolColor: UIColor {
Colors.appWhite
}
var symbolFont: UIFont {
UIFont.systemFont(ofSize: 13)
var iconImage: Subscribable<TokenImage> {
token.icon
}
}

@ -58,19 +58,7 @@ struct FungibleTokenViewCellViewModel {
return isVisible ? 1.0 : 0.4
}
var iconImage: UIImage {
token.icon.image
}
var symbolInIcon: String {
token.icon.symbol
}
var symbolColor: UIColor {
Colors.appWhite
}
var symbolFont: UIFont {
UIFont.systemFont(ofSize: 13)
var iconImage: Subscribable<TokenImage> {
token.icon
}
}

@ -86,19 +86,7 @@ struct NonFungibleTokenViewCellViewModel {
return isVisible ? 1.0 : 0.4
}
var iconImage: UIImage {
token.icon.image
}
var symbolInIcon: String {
token.icon.symbol
}
var symbolColor: UIColor {
Colors.appWhite
}
var symbolFont: UIFont {
UIFont.systemFont(ofSize: 13)
var iconImage: Subscribable<TokenImage> {
token.icon
}
}

@ -9,7 +9,6 @@ class EthTokenViewCell: UITableViewCell {
private let background = UIView()
private let titleLabel = UILabel()
private let symbolLabel = UILabel()
private let valuePercentageChangeValueLabel = UILabel()
private let valuePercentageChangePeriodLabel = UILabel()
private let valueChangeLabel = UILabel()
@ -18,9 +17,8 @@ class EthTokenViewCell: UITableViewCell {
private var viewsWithContent: [UIView] {
[titleLabel, valuePercentageChangeValueLabel, valuePercentageChangePeriodLabel, valueChangeLabel]
}
private var tokenIconImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
private var tokenIconImageView: TokenImageView = {
let imageView = TokenImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
@ -44,12 +42,7 @@ class EthTokenViewCell: UITableViewCell {
stackView.translatesAutoresizingMaskIntoConstraints = false
background.addSubview(stackView)
symbolLabel.translatesAutoresizingMaskIntoConstraints = false
background.addSubview(symbolLabel)
NSLayoutConstraint.activate([
symbolLabel.anchorsConstraint(to: tokenIconImageView),
tokenIconImageView.heightAnchor.constraint(equalToConstant: 40),
tokenIconImageView.widthAnchor.constraint(equalToConstant: 40),
stackView.anchorsConstraint(to: background, edgeInsets: .init(top: 16, left: 20, bottom: 16, right: 16)),
@ -74,12 +67,6 @@ class EthTokenViewCell: UITableViewCell {
titleLabel.text = "\(viewModel.amount) \(viewModel.title)"
titleLabel.baselineAdjustment = .alignCenters
symbolLabel.textColor = viewModel.symbolColor
symbolLabel.font = viewModel.symbolFont
symbolLabel.textAlignment = .center
symbolLabel.adjustsFontSizeToFitWidth = true
symbolLabel.text = viewModel.symbolInIcon
valuePercentageChangeValueLabel.textColor = viewModel.valuePercentageChangeColor
valuePercentageChangeValueLabel.font = viewModel.textValueFont
valuePercentageChangeValueLabel.text = viewModel.valuePercentageChangeValue
@ -100,6 +87,6 @@ class EthTokenViewCell: UITableViewCell {
viewsWithContent.forEach {
$0.alpha = viewModel.alpha
}
tokenIconImageView.image = viewModel.iconImage
tokenIconImageView.subscribable = viewModel.iconImage
}
}

@ -9,14 +9,12 @@ class FungibleTokenViewCell: UITableViewCell {
private let background = UIView()
private let titleLabel = UILabel()
private let symbolLabel = UILabel()
private let blockchainLabel = UILabel()
private var viewsWithContent: [UIView] {
[self.titleLabel, blockchainLabel]
}
private var tokenIconImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
private var tokenIconImageView: TokenImageView = {
let imageView = TokenImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
@ -36,12 +34,7 @@ class FungibleTokenViewCell: UITableViewCell {
stackView.translatesAutoresizingMaskIntoConstraints = false
background.addSubview(stackView)
symbolLabel.translatesAutoresizingMaskIntoConstraints = false
background.addSubview(symbolLabel)
NSLayoutConstraint.activate([
symbolLabel.anchorsConstraint(to: tokenIconImageView),
tokenIconImageView.heightAnchor.constraint(equalToConstant: 40),
tokenIconImageView.widthAnchor.constraint(equalToConstant: 40),
stackView.anchorsConstraint(to: background, edgeInsets: .init(top: 16, left: 20, bottom: 16, right: 16)),
@ -66,12 +59,6 @@ class FungibleTokenViewCell: UITableViewCell {
titleLabel.text = "\(viewModel.amount) \(viewModel.title)"
titleLabel.baselineAdjustment = .alignCenters
symbolLabel.textColor = viewModel.symbolColor
symbolLabel.font = viewModel.symbolFont
symbolLabel.textAlignment = .center
symbolLabel.adjustsFontSizeToFitWidth = true
symbolLabel.text = viewModel.symbolInIcon
blockchainLabel.textColor = viewModel.subtitleColor
blockchainLabel.font = viewModel.subtitleFont
blockchainLabel.text = viewModel.blockChainName
@ -80,6 +67,6 @@ class FungibleTokenViewCell: UITableViewCell {
$0.alpha = viewModel.alpha
}
tokenIconImageView.image = viewModel.iconImage
tokenIconImageView.subscribable = viewModel.iconImage
}
}

@ -9,14 +9,12 @@ class NonFungibleTokenViewCell: UITableViewCell {
private let background = UIView()
private let titleLabel = UILabel()
private let symbolLabel = UILabel()
private let blockchainLabel = UILabel()
private var viewsWithContent: [UIView] {
[self.titleLabel, self.blockchainLabel]
}
private var tokenIconImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
private var tokenIconImageView: TokenImageView = {
let imageView = TokenImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
@ -36,12 +34,7 @@ class NonFungibleTokenViewCell: UITableViewCell {
stackView.translatesAutoresizingMaskIntoConstraints = false
background.addSubview(stackView)
symbolLabel.translatesAutoresizingMaskIntoConstraints = false
background.addSubview(symbolLabel)
NSLayoutConstraint.activate([
symbolLabel.anchorsConstraint(to: tokenIconImageView),
tokenIconImageView.heightAnchor.constraint(equalToConstant: 40),
tokenIconImageView.widthAnchor.constraint(equalToConstant: 40),
stackView.anchorsConstraint(to: background, edgeInsets: .init(top: 16, left: 20, bottom: 16, right: 16)),
@ -66,12 +59,6 @@ class NonFungibleTokenViewCell: UITableViewCell {
titleLabel.text = "\(viewModel.amount) \(viewModel.title)"
titleLabel.baselineAdjustment = .alignCenters
symbolLabel.textColor = viewModel.symbolColor
symbolLabel.font = viewModel.symbolFont
symbolLabel.textAlignment = .center
symbolLabel.adjustsFontSizeToFitWidth = true
symbolLabel.text = viewModel.symbolInIcon
blockchainLabel.textColor = viewModel.subtitleColor
blockchainLabel.font = viewModel.subtitleFont
blockchainLabel.text = viewModel.blockChainName
@ -79,6 +66,6 @@ class NonFungibleTokenViewCell: UITableViewCell {
viewsWithContent.forEach {
$0.alpha = viewModel.alpha
}
tokenIconImageView.image = viewModel.iconImage
tokenIconImageView.subscribable = viewModel.iconImage
}
}

@ -29,26 +29,15 @@ class SelectCurrencyButton: UIControl {
return imageView
}()
private let currencyIconImageView: UIImageView = {
let imageView = UIImageView(image: #imageLiteral(resourceName: "eth"))
private let currencyIconImageView: TokenImageView = {
let imageView = TokenImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.setContentHuggingPriority(.required, for: .horizontal)
imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
imageView.contentMode = .scaleAspectFit
return imageView
}()
private let symbolLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = Colors.appWhite
label.font = UIFont.systemFont(ofSize: 13)
label.textAlignment = .center
label.adjustsFontSizeToFitWidth = true
return label
}()
private let actionButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
@ -71,18 +60,12 @@ class SelectCurrencyButton: UIControl {
}
}
var image: UIImage? {
var tokenIcon: Subscribable<TokenImage>? {
get {
return currencyIconImageView.image
currencyIconImageView.subscribable
}
set {
currencyIconImageView.image = newValue
}
}
var symbolInIcon: String = "" {
didSet {
symbolLabel.text = symbolInIcon
currencyIconImageView.subscribable = newValue
}
}
@ -104,14 +87,11 @@ class SelectCurrencyButton: UIControl {
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
addSubview(actionButton)
addSubview(symbolLabel)
setContentHuggingPriority(.required, for: .horizontal)
setContentCompressionResistancePriority(.required, for: .horizontal)
NSLayoutConstraint.activate([
symbolLabel.anchorsConstraint(to: currencyIconImageView),
stackView.anchorsConstraint(to: self),
actionButton.anchorsConstraint(to: self),
currencyIconImageView.widthAnchor.constraint(equalToConstant: 40),

@ -0,0 +1,62 @@
// Copyright © 2020 Stormbird PTE. LTD.
import UIKit
class TokenImageView: UIView {
private var subscriptionKey: Subscribable<TokenImage>.SubscribableKey?
private let symbolLabel: UILabel = {
let label = UILabel()
label.textColor = Colors.appWhite
label.font = UIFont.systemFont(ofSize: 13)
label.textAlignment = .center
label.adjustsFontSizeToFitWidth = true
return label
}()
private var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}()
var subscribable: Subscribable<TokenImage>? {
didSet {
if let previousSubscribable = oldValue, let subscriptionKey = subscriptionKey {
previousSubscribable.unsubscribe(subscriptionKey)
}
if let subscribable = subscribable {
subscriptionKey = subscribable.subscribe { [weak self] imageAndSymbol in
guard let strongSelf = self else { return }
strongSelf.imageView.image = imageAndSymbol?.image
strongSelf.symbolLabel.text = imageAndSymbol?.symbol ?? ""
}
} else {
subscriptionKey = nil
imageView.image = nil
symbolLabel.text = ""
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
symbolLabel.translatesAutoresizingMaskIntoConstraints = false
addSubview(symbolLabel)
NSLayoutConstraint.activate([
symbolLabel.anchorsConstraint(to: imageView),
imageView.anchorsConstraint(to: self),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

@ -1,11 +1,14 @@
// Copyright © 2020 Stormbird PTE. LTD.
import UIKit
import PromiseKit
typealias TokenImage = (image: UIImage, symbol: String)
extension TokenObject {
private static let numberOfCharactersOfSymbolToShow = 4
fileprivate static let numberOfCharactersOfSymbolToShowInIcon = 4
private var programmaticallyGeneratedIconImage: UIImage {
fileprivate var programmaticallyGeneratedIconImage: UIImage {
UIView.tokenSymbolBackgroundImage(backgroundColor: symbolBackgroundColor)
}
@ -25,21 +28,113 @@ extension TokenObject {
}
}
var icon: (image: UIImage, symbol: String) {
let image: UIImage?
var icon: Subscribable<TokenImage> {
switch type {
case .nativeCryptocurrency:
image = server.iconImage
if let img = server.iconImage {
return .init((image: img, symbol: ""))
}
case .erc20, .erc875, .erc721, .erc721ForTickets:
image = nil
break
}
return TokenImageFetcher.instance.image(forToken: self)
}
}
fileprivate class TokenImageFetcher {
private enum ImageAvailabilityError: LocalizedError {
case notAvailable
}
static var instance = TokenImageFetcher()
if let img = image {
return (image: img, symbol: "")
private var subscribables: [String: Subscribable<TokenImage>] = .init()
private func programmaticallyGenerateIcon(forToken tokenObject: TokenObject) -> TokenImage {
let i = [TokenObject.numberOfCharactersOfSymbolToShowInIcon, tokenObject.symbol.count].min()!
let symbol = tokenObject.symbol.substring(to: i)
return (image: tokenObject.programmaticallyGeneratedIconImage, symbol: symbol)
}
//Relies on built-in HTTP/HTTPS caching in iOS for the images
func image(forToken tokenObject: TokenObject) -> Subscribable<TokenImage> {
let subscribable: Subscribable<TokenImage>
let key = "\(tokenObject.contractAddress.eip55String)-\(tokenObject.server.chainID)"
if let sub = subscribables[key] {
subscribable = sub
} else {
let i = [TokenObject.numberOfCharactersOfSymbolToShow, symbol.count].min()!
return (image: programmaticallyGeneratedIconImage, symbol: symbol.substring(to: i))
let sub = Subscribable<TokenImage>(nil)
subscribables[key] = sub
subscribable = sub
}
if tokenObject.contractAddress.sameContract(as: Constants.nativeCryptoAddressInDatabase) {
subscribable.value = programmaticallyGenerateIcon(forToken: tokenObject)
return subscribable
}
fetchFromOpenSea(tokenObject).done {
subscribable.value = (image: $0, symbol: "")
}.catch { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.fetchFromAssetGitHubRepo(tokenObject).done {
subscribable.value = (image: $0, symbol: "")
}.catch { [weak self] _ in
guard let strongSelf = self else { return }
subscribable.value = strongSelf.programmaticallyGenerateIcon(forToken: tokenObject)
}
}
return subscribable
}
private func fetchFromOpenSea(_ tokenObject: TokenObject) -> Promise<UIImage> {
Promise { seal in
switch tokenObject.type {
case .erc721:
if let json = tokenObject.balance.first?.balance, let data = json.data(using: .utf8), let openSeaNonFungible = try? JSONDecoder().decode(OpenSeaNonFungible.self, from: data), !openSeaNonFungible.contractImageUrl.isEmpty {
let request = URLRequest(url: URL(string: openSeaNonFungible.contractImageUrl)!)
fetch(request: request).done { image in
seal.fulfill(image)
}.catch { error in
seal.reject(ImageAvailabilityError.notAvailable)
}
}
case .nativeCryptocurrency, .erc20, .erc875, .erc721ForTickets:
seal.reject(ImageAvailabilityError.notAvailable)
}
}
}
private func fetchFromAssetGitHubRepo(_ tokenObject: TokenObject) -> Promise<UIImage> {
Promise { seal in
let request = URLRequest(url: URL(string: "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/\(tokenObject.contractAddress.eip55String)/logo.png")!)
fetch(request: request).done { image in
seal.fulfill(image)
}.catch { error in
seal.reject(ImageAvailabilityError.notAvailable)
}
}
}
private func fetch(request: URLRequest) -> Promise<UIImage> {
Promise { seal in
do {
try NSURLConnection.sendAsynchronousRequest(request, queue: .main) { _, data, error in
if let data = data {
let image = UIImage(data: data)
if let img = image {
seal.fulfill(img)
} else {
seal.reject(ImageAvailabilityError.notAvailable)
}
} else {
seal.reject(ImageAvailabilityError.notAvailable)
}
}
} catch {
seal.reject(ImageAvailabilityError.notAvailable)
}
}
}
}

@ -79,12 +79,12 @@ class AmountTextField: UIControl {
case cryptoCurrency(TokenObject)
case usd
var icon: UIImage {
var icon: Subscribable<TokenImage> {
switch self {
case .cryptoCurrency(let tokenObject):
return tokenObject.icon.image
return tokenObject.icon
case .usd:
return R.image.usaFlag()!
return .init((image: R.image.usaFlag()!, symbol: ""))
}
}
@ -96,15 +96,6 @@ class AmountTextField: UIControl {
return "USD"
}
}
var symbolInIcon: String {
switch self {
case .cryptoCurrency(let tokenObject):
return tokenObject.icon.symbol
case .usd:
return ""
}
}
}
struct Pair {
@ -304,8 +295,7 @@ class AmountTextField: UIControl {
private func update(selectCurrencyButton button: SelectCurrencyButton) {
button.text = currentPair.left.symbol
button.image = currentPair.left.icon
button.symbolInIcon = currentPair.left.symbolInIcon
button.tokenIcon = currentPair.left.icon
}
@objc func fiatAction(button: UIButton) {

Loading…
Cancel
Save