지난 글에서 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은 그런 느낌이 들려면 무작정 많이 만들어보면서 학습해야 할 것 같아요.
홧팅입니다. :)
'Project > UIKit 업비트' 카테고리의 다른 글
[UIKit] TableView에서 insetGrouped 스타일로 섹션을 나눠보자 (0) | 2023.04.22 |
---|---|
[UIKit] TableView 위에 Header를 만들어보자 (0) | 2023.04.22 |
[UIKit] UISearchController, 검색 기능 구현하기 (1) | 2023.04.17 |
[UIKit] CollectionView가 먼저 생성되고 데이터가 갱신되지 않는 문제 (0) | 2023.04.07 |
[UIKit] 탭 바 배경색이 투명할 때 해결방법 (0) | 2022.12.17 |
댓글