본문 바로가기
Project/UIKit 업비트

[UIKit] 업비트 API로 CollectionView에 리스트 만들기 feat.RxCocoa

by iOS_woo 2023. 4. 13.

지난 글에서 API로 데이터를 받아온 이후에 CollectionView가 새롭게 그려지지 않았던 문제를 해결하였습니다. 

UIKit 강의를 보니 데이터가 변경되었을 때 직접 reloadData()를 호출해서 갱신해주어야 하더라고요.


@escaping 혹은 asycn/await을 사용해서 데이터를 받아온 이후 실행하는 방법도 있었지만, 
저는 싱글톤 패턴을 사용해서 하나의 변수에 코인 리스트를 저장해서 변수의 값이 업데이트되었을 때 reloadData()를 호출하는 방법을 선택하였습니다. 

import Foundation
import RxCocoa

class UpbitRestApiService {
    
    static let shared = UpbitRestApiService()
    
    let coinsSubject = BehaviorRelay<[UpbitCoin]>(value: [])
    var coins: [UpbitCoin] { coinsSubject.value }

    let tickersSubject = BehaviorRelay<[String: UpbitTicker]>(value: [:])
    var tickers: [String: UpbitTicker] { tickersSubject.value }
    
    private let baseUrl = "https://api.upbit.com/v1"
    
    private init() {
        Task {
            await fetchCoins()
        }
    }
    
    func fetchCoins() async {
        let request = URLRequest(url: URL(string: "\(baseUrl)/market/all")!)
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else {
                print("Error fetching upbit coins")
                return
            }
            let coins = try JSONDecoder().decode([UpbitCoin].self, from: data).filter { $0.market.hasPrefix("KRW") }
            self.coinsSubject.accept(coins)
            await fetchTickers()
        } catch {
            print("Error fetching upbit coins: \(error)")
        }
    }
    
    func fetchTickers() async {
        let tickersUrl = "https://api.upbit.com/v1/ticker?markets=" + coins.map { $0.market }.joined(separator: ",")
        let request = URLRequest(url: URL(string: tickersUrl)!)
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else {
                print("Error fetching upbit tickers")
                return
            }
            let tickersRestAPI = try JSONDecoder().decode([UpbitTickerRestAPI].self, from: data)
            var tickersDict = [String: UpbitTicker]()
            tickersRestAPI.forEach { tickerRestAPI in
                let ticker = UpbitTicker(market: tickerRestAPI.market,
                                         change: tickerRestAPI.change,
                                         tradePrice: tickerRestAPI.tradePrice,
                                         changeRate: tickerRestAPI.changeRate,
                                         accTradePrice24H: tickerRestAPI.accTradePrice24H,
                                         signedChangeRate: tickerRestAPI.signedChangeRate)
                tickersDict[ticker.market] = ticker
            }
            self.tickersSubject.accept(tickersDict)
        } catch {
            print("Error fetching upbit tickers: \(error)")
        }
    }
}

코드를 보시면 BehaviorRelay를 이용해서 변수를 만들어주었고, 
fetchTicker() 함수에서 .accept()를 사용해 BehaviorRelay를 업데이트해주고 있어요. 

BehaviorRelay는 정보가 업데이트 되면  구독하고 있는 구독자들에게 뿌려주는 역할을 해요. (이 부분은 더 깊이 공부해서 따로 글을 작성해보겠습니다!)


다음과 같이 구독자를 생성하고 업데이트가 되었을 때 데이터를 다룰 수 있어요. 

func fetchTickersFromRestApi() {
        dataService.tickersSubject
            .observe(on: MainScheduler.asyncInstance)
            .subscribe(onNext: { tickers in
                Task {
                    await self.updateWinners(tickers: tickers)
                    await self.updateLossers(tickers: tickers)
                    await self.updateVolume(tickers: tickers)
                }
            })
            .disposed(by: disposeBag)
    }

RxSwift를 사용해서 tickerSubject를 구독하는 구독자를 생성하고, 오퍼레이터를 사용해서 데이터를 가공하는 작업을 하고 있어요. 
tickerSubject에 데이터가 갱신될 때마다 실행돼요.

데이터가 갱신되면 맨 위에 보여드린 함수를 사용해서 reloadData()가 실행되고 viewModel의 volume 변수를 기반으로 셀이 그려져요. 

cell.configure(with: vm.volume[indexPath.row])로 셀에 viewModel을 생성해주고 있어요. 


collectionViewCell은 다음과 같이 생겼습니다. 

class TickerCell: UICollectionViewCell {
    // MARK: - Properties
    
    private var vm: TickerViewModel? {
        didSet { configureLabels() }
    }
    
    private let symbolLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.boldSystemFont(ofSize: 13)
        return label
    }()
    
    private let priceLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.boldSystemFont(ofSize: 20)
        label.textColor = .black
        return label
    }()
    
    private let changeRateLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16)
        return label
    }()
    
    private let volumeLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16)
        return label
    }()
    
    // MARK: - Lifecycles
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        configureUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - Configures
    
    func configureUI() {
        backgroundColor = .white
        
        addSubview(symbolLabel)
        symbolLabel.anchor(top: topAnchor, left: leftAnchor, paddingTop: 10, paddingLeft: 20)
        
        addSubview(priceLabel)
        priceLabel.anchor(top: symbolLabel.bottomAnchor, left: leftAnchor, paddingTop: 8, paddingLeft: 20)
        
        addSubview(changeRateLabel)
        changeRateLabel.anchor(top: topAnchor, right: rightAnchor, paddingTop: 10, paddingRight: 20)
        
        addSubview(volumeLabel)
        volumeLabel.anchor(top: changeRateLabel.bottomAnchor, right: rightAnchor, paddingTop: 8, paddingRight: 20)
    }
    
    // MARK: - Helpers
    
    func configure(with vm: TickerViewModel) {
        self.vm = vm
    }
    
    func configureLabels() {
        guard let vm = vm else { return }
        
        symbolLabel.text = vm.symbol
        priceLabel.text = vm.price
        changeRateLabel.text = vm.changeRate
        volumeLabel.text = vm.volume
    }
}


 configure() 함수로 viewModel을 전달받으면 didSet을 사용해서 configureLabels()를 실행해서 cell을 업데이트 해줘요!

UIKit과 RxSwift, RxCocoa를 사용해서 업비트 코인 리스트를 보여주는데 성공하였습니다!
웹소켓으로 실시간 데이터를 변경해주는 것은 생각보다 어려운 작업은 아닐 것 같아요. 
이미 블록와이드 프로젝트로 성공시킨 경험이 있기 때문이죠!

추가로 강의에서 공부한 @escaping을 활용해서 collectionView를 새롭게 그려주는 방식도 기록해볼게요. 

User 데이터를 전달해주는 fetchUser() 함수를 작성해줘요. 

 

UICollectionViewController에서 함수를 호출하고  전달받은 데이터를 user라는 변수에 업데이트 해줘요.

user가 업데이트되면 didSet 을 사용해서 collectionView.reloadData()를 실행합니다.
완료!

async/await으로도 @escaping과 비슷하게 구현할 수 있어요. 
throws 와 try await  등으로 구현할 수 있는데 구현이 어렵지는 않아서 추후 업로드해볼게요! 


앞으로 도전과제와 공부할 내용은 다음과 같아요. 
1. 소켓을 활용해 실시간 업데이트 구현하기
2. UIKit을 더 공부해서 업비트 메인 화면과 같은 화면 클론해보기
3. RxSwift에 대해서 더 깊게 공부하기
등등.. 

SwiftUI로는 어떤 앱이든 시간만 있다면 구현할 수 있겠다는 느낌이 있는데, 
아직 UIKit은 그런 느낌이 들려면 무작정 많이 만들어보면서 학습해야 할 것 같아요. 
홧팅입니다. :)

댓글