swift로 코인 앱 만들기 Part1. Coins

25829 단어 iOSswiftiOS

본문에 들어가기에 앞서, 이번 앱은 Nomad Coders에서 제공하는 챌린지 프로그램인 React Native 챌린지에서 나온 앱(Coin Tracker였나?)인데, React Native로 만든 앱은 아니고, 제목에 썼다시피 필자가 스스로 Swift로 재구성(?)해서 만든 앱이다.
Nomad Coders에는 챌린지 내용 공유 금지가 있지만 이 앱은 Swift로 만든 앱으로 Nomad Coders에서는 제공하지 않는 프로그래밍 언어이기에 운영자님께 Swift면 괜찮다고 해서 이 앱을 포스팅 한다.
서두가 길었다. 시작한다.

앱의 구성부터




(이미지가 좀 크네... 출처는 Doeveloper님의 챌린지 우수작 결과물로부터)

총 4개의 Tab bar navigation으로 이루어진 앱이며, 각각 코인 목록과 Detail, 코인 가격, 코인 관련 뉴스, Discover(뭐하는 건지는 모르겠다. 1기 챌린지를 하다 말아서...)로 구성되어 있다.

맨 처음부터 차례대로 만들어 보기로 한다.

Storyboard 구성

... 안 보이쥬?

시작은 일단 Tab Bar Controller로 시작한다.
여기에서 네 개의 화면으로 뻗어나간다.

Coins 화면

Navigation Controller - View Controller - View Controller(Detail)로 구성되어 있다.가운데는 위의 스샷에서 보는것 처럼 코인 목록을 rank순으로 row당 3개씩 보여주는 Collection View Controller다.

소스

import UIKit
import Alamofire
import Kingfisher


class CoinsViewController: UICollectionViewController {
    
    @IBOutlet weak var indicator: UIActivityIndicatorView!
    var cellsPerRow: CGFloat = 3
    let cellMargin: CGFloat = 10
    
    var coins: [Coin] = [] {
        didSet {
            collectionView.reloadData()
        }
    }
       
    override func viewDidLoad() {
        super.viewDidLoad()
        let nibName = UINib(nibName: "CoinCell", bundle: nil)
        collectionView.register(nibName, forCellWithReuseIdentifier: "CoinCell")
        
        self.indicator.startAnimating()
        self.fetchCoinsData(completionHandler: { [weak self] result in
            guard let self = self else { return }
            self.indicator.stopAnimating()
            self.indicator.isHidden = true
            
            switch result {
            case let .success(result):
                self.coins = result                
            case let .failure(error):
                debugPrint("Error: \(error)")
            }
        })
    }
    
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return coins.count
    }
        
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CoinCell", for: indexPath) as? CoinCell else { return UICollectionViewCell() }
        
        let imageURL = URL(string: "https://cryptoicon-api.vercel.app/api/icon/\(coins[indexPath.item].symbol.lowercased())")
        
        cell.coinSymbolLabel.text = coins[indexPath.item].name
        cell.coinSymbolImageView.kf.setImage(with: imageURL)
                
        cell.contentView.layer.cornerRadius = 2.0
        cell.layer.shadowColor = UIColor.black.cgColor
        cell.layer.shadowOpacity = 0.5
        return cell
    }
    
    private func fetchCoinsData(completionHandler: @escaping (Result<[Coin], Error>) -> Void) {
        let url = "https://api.coinpaprika.com/v1/coins"
        
        AF.request(url, method: .get)
            .responseData(completionHandler: { response in
                switch response.result {
                case let .success(data):
                    do {
                        let decoder = JSONDecoder()
                        let result = try decoder.decode([Coin].self, from: data)
                        completionHandler(.success(result))
                        
                    } catch {
                        completionHandler(.failure(error))
                    }
                case let .failure(error):
                    completionHandler(.failure(error))
                }
            })
    }
    
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
        guard let detailViewController = storyboard.instantiateViewController(identifier: "CoinDetailViewController") as? CoinDetailViewController else { return }
        
        detailViewController.coinId = self.coins[indexPath.item].id
        detailViewController.title = self.coins[indexPath.item].name
        self.show(detailViewController, sender: nil)
    }
}

extension CoinsViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = collectionView.frame.width
        let cellWidth = (width - 4 * cellMargin) / 3
        return CGSize(width: cellWidth, height: cellWidth)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }
}

API 호출을 조금 간결하게 만들어주는 Alamofire Library를 사용하였으며 이미지 셋에는 Kingfisher(킹피셔)를 사용했다.

코인 관련 구조체

struct Coin: Codable {
    let id: String
    let name: String
    let symbol: String
    let rank: Int    
}

struct CoinDetail: Codable {
    let id: String
    let name: String
    let links: [Link]
    let description: String
    
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case description
        case links = "links_extended"
    }
}

API는 https://api.coinpaprika.com/v1/coins 에서 호출하며, detail은 url 뒤에 /코인id값을 넣는 식이다. (/btc-bitcoin 같은)

코인 상세 페이지

about 코인명의 타이틀을 가지며, description을 준다. description은 UITextView로 height 100으로 고정시켰다.
바로 밑에는 관련 링크들인데 눌러보진 않았다(....)
링크 버튼을 누르면 연결된 페이지로 브라우저 연결 되는 식이다.

좋은 웹페이지 즐겨찾기