<file_sep>//
//  ImageURLView.swift
//  CryptoWorkshop
//
//  Created by <NAME> on 8/31/18.
//  Copyright © 2018 TanookiLabs. All rights reserved.
//

import UIKit

class ImageURLView: UIView {

    let imageUrl: URL
    let imageView: UIImageView
    let errorLabel: UILabel
    let activityIndicator: UIActivityIndicatorView
    
    private var task: URLSessionTask?
    
    // MARK: - Life Cycle
    
    init(imageUrl: URL, frame: CGRect = CGRect(x: 0, y: 0, width: 200, height: 200)) {
        self.imageUrl = imageUrl
        self.imageView = UIImageView(frame: .zero)
        self.errorLabel = UILabel(frame: .zero)
        self.activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
        super.init(frame: frame)
        initialize()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func initialize() {
        setupLayout()
        loadImage()
    }
    
    // MARK: - Configuration
    
    private func setupLayout() {
        imageView.contentMode = .scaleAspectFit
        imageView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(imageView)
        
        errorLabel.numberOfLines = 1
        errorLabel.font = UIFont.systemFont(ofSize: 10)
        errorLabel.textAlignment = .center
        errorLabel.translatesAutoresizingMaskIntoConstraints = false
        addSubview(errorLabel)
        
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        addSubview(activityIndicator)
        
        imageView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
        imageView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
        imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        
        activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        
        errorLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 10).isActive = true
        errorLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: 10).isActive = true
        errorLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        
        heightAnchor.constraint(equalToConstant: 200).isActive = true
        
        setNeedsDisplay()
        layoutIfNeeded()
    }
    
    private func loadImage() {
        showSpinner()
        setError(message: "")
        
        let session = URLSession.shared
        let imageUrl = self.imageUrl.pathExtension == "svg" ? ZippySVG(imageUrl: self.imageUrl) : self.imageUrl
        
        task = session.dataTask(with: imageUrl, completionHandler: { [weak self] (data, response, error) in
            guard let `self` = self else { return }
            
            defer {
                self.hideSpinner()
            }
            
            if let error = error {
                self.setError(message: error.localizedDescription)
                return
            }
            
            if let response = response as? HTTPURLResponse,
                response.statusCode != 200 {
                self.setError(message: "Server response: \(response.statusCode)")
                return
            }
            
            if let data = data,
                let image = UIImage(data: data) {
                self.setImage(image)
            } else {
                self.setError(message: "Unable to decode image")
            }
        })
        
        task?.resume()
    }
    
    // MARK: - UI Actions
    
    private func setError(message: String) {
        DispatchQueue.main.async {
            self.errorLabel.text = message
        }
    }
    
    private func setImage(_ image: UIImage) {
        DispatchQueue.main.async {
            self.imageView.image = image
        }
    }
    
    private func showSpinner() {
        DispatchQueue.main.async {
            self.activityIndicator.startAnimating()
            self.activityIndicator.isHidden = false
        }
    }
    
    private func hideSpinner() {
        DispatchQueue.main.async {
            self.activityIndicator.stopAnimating()
            self.activityIndicator.isHidden = true
        }
    }
    
}

// MARK: - ZippySVG

fileprivate func ZippySVG(imageUrl: URL) -> URL {
    let baseURL = URL(string: "https://zippy-svg.herokuapp.com/svg")!
    var zippyURL = URLComponents(url: baseURL, resolvingAgainstBaseURL: true)!
    zippyURL.queryItems = [URLQueryItem(name: "url", value: imageUrl.absoluteString)]
    return zippyURL.url!
}
<file_sep># Cryptocurrency Workshop @ try! Swift

Materials for the Cryptocurrency Workshop at the [2018 try! Swift NYC](https://www.tryswift.co/events/2018/nyc/) conference.

## Getting started

Please download this repository to get started:

```
git clone https://github.com/mseijas/crypto-workshop.git
```

## Notes

The completed version of the project can be found on the `final` branch
<file_sep>//
//  TokenMetadata.swift
//  CryptoWorkshop
//
//  Created by <NAME> on 8/31/18.
//  Copyright © 2018 TanookiLabs. All rights reserved.
//

import Foundation

struct TokenMetada: Codable {
    let name: String
    let description: String
    let image: String
}
<file_sep>platform :ios, '11.4'
inhibit_all_warnings!

target 'CryptoWorkshop' do
  use_frameworks!

  # Pods for CryptoWorkshop
  pod 'web3swift'
end
<file_sep>//
//  ViewController.swift
//  CryptoWorkshop
//
//  Created by <NAME> on 8/31/18.
//  Copyright © 2018 TanookiLabs. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var stackView: UIStackView!
    @IBOutlet weak var assetCountLabel: UILabel!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!
    
    let tokenMetadataProvider = TokenMetadaProvider()
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    
        let web3 = setupWeb3()
        
        // CryptoStrikers contract
        
        // Ethreum user account
        
        // Populate count label
        
        // Get Token metadata for each token
        
        // Render UI

        activityIndicator.stopAnimating()
    }
    
    // 1. Setup Ethereum node + web3 object
    func setupWeb3() -> web3 {
        
    }

    // 2. Setup contract with ABI
    func setupContract(with web3: web3, address: EthereumAddress) -> web3.web3contract {
        
    }
    
    // 3. Interact with the contract
    // 3a. Get token balance for an account
    func tokenBalance(for account: EthereumAddress, on contract: web3.web3contract) -> Int {
        
    }
    
    // 3b. Get token id by index
    func tokenId(for account: EthereumAddress, index: Int, on contract: web3.web3contract) -> Int {
        
    }
    
    // 3c. Get token metadata url by id
    func tokenMetadataURL(id: Int, on contract: web3.web3contract) -> URL {
        
    }
    
}
<file_sep>//
//  TokenMetadataProvider.swift
//  CryptoWorkshop
//
//  Created by <NAME> on 8/31/18.
//  Copyright © 2018 TanookiLabs. All rights reserved.
//

import Foundation

class TokenMetadaProvider {
    
    func getMetadata(with url: URL) -> TokenMetada? {
        let session = URLSession.shared
        let semaphore = DispatchSemaphore(value: 0)
        
        var tokenMetadata: TokenMetada?
        
        let task = session.dataTask(with: url) { (data, response, error) in
            if let error = error {
                print("Error: \(error)")
                tokenMetadata = nil
            }
            
            if let response = response as? HTTPURLResponse,
                response.statusCode != 200 {
                print("Error: Server responded with \(response.statusCode)")
                tokenMetadata = nil
            }
            
            let decoder = JSONDecoder()
            if let data = data {
                do {
                    let response = try decoder.decode(TokenMetada.self, from: data)
                    tokenMetadata = response
                } catch {
                    print("Error: \(error.localizedDescription)")
                }
            } else {
                print("Error: JSON parsing error")
                tokenMetadata = nil
            }
            
            semaphore.signal()
        }
        
        task.resume()
        _ = semaphore.wait(timeout: DispatchTime.distantFuture)
        
        return tokenMetadata
    }
    
}
