Downloading Coin Images using Combine, MVVM, and a Networking Layer | SwiftUI Crypto App #8
private func addSubscription() {
dataService
.$image
.sink { _ in
self.isLoading = false
} receiveValue: { [weak self] image in
self?.image = image
}
.store(in: &cancellables)
}
func fetchCoinImages(urlString: String) {
guard let url = URL(string: urlString) else { return }
imageSubscription = NetworkingManager
.download(with: url)
.tryMap({ data -> UIImage? in
return UIImage(data: data)
})
.sink(receiveCompletion: NetworkingManager.handleCompletion,
receiveValue: { [weak self] image in
self?.image = image
self?.imageSubscription?.cancel()
})
}
if let image = viewModel.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
} else if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "questionmark")
.foregroundColor(Color.theme.secondaryText)
}
image
데이터 퍼블리셔 값에 따라 뷰를 그리는 부분isLoading
변수 값을 통해 뷰 모델이 이니셜라이즈되었는지 확인, 이후 리턴받은 데이터 상태에 따라 패치된 이미지 또는 디폴트 이미지 표현private var leftCoulmn: some View {
HStack(spacing: 0) {
Text("\(coin.rank)")
.font(.caption)
.foregroundColor(Color.theme.secondaryText)
.frame(minWidth: 30)
CoinImageView(coin: coin)
.frame(width: 30, height: 30)
Text(coin.symbol.uppercased())
.font(.headline)
.padding(.leading, 6)
.foregroundColor(Color.theme.accent)
}
}
CoinRowView
를 그릴 때 사용한 Circle
뷰가 아니라 해당 CoinImageView
로 대체import Foundation
import SwiftUI
import Combine
class CoinImageService {
@Published var image: UIImage? = nil
private var imageSubscription: AnyCancellable?
private let coin: CoinModel
init(coin: CoinModel) {
self.coin = coin
fetchCoinImages(urlString: coin.image)
}
func fetchCoinImages(urlString: String) {
guard let url = URL(string: urlString) else { return }
imageSubscription = NetworkingManager
.download(with: url)
.tryMap({ data -> UIImage? in
return UIImage(data: data)
})
.sink(receiveCompletion: NetworkingManager.handleCompletion,
receiveValue: { [weak self] image in
self?.image = image
self?.imageSubscription?.cancel()
})
}
}
import Foundation
import SwiftUI
import Combine
class CoinImageViewModel: ObservableObject {
@Published var image: UIImage? = nil
@Published var isLoading: Bool = false
private let coin: CoinModel
private let dataService: CoinImageService
private var cancellables = Set<AnyCancellable>()
init(coin: CoinModel) {
self.coin = coin
self.dataService = CoinImageService(coin: coin)
addSubscription()
isLoading = true
}
private func addSubscription() {
dataService
.$image
.sink { _ in
self.isLoading = false
} receiveValue: { [weak self] image in
self?.image = image
}
.store(in: &cancellables)
}
}
CoinImageView
가 초기화될 때 함께 이니셜라이즈되는 뷰 모델init
단에서 건네받은 coin
데이터를 통해 addSubscription
등을 사용import SwiftUI
struct CoinImageView: View {
@StateObject private var viewModel: CoinImageViewModel
init(coin: CoinModel) {
_viewModel = StateObject(wrappedValue: CoinImageViewModel(coin: coin))
}
var body: some View {
ZStack {
if let image = viewModel.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
} else if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "questionmark")
.foregroundColor(Color.theme.secondaryText)
}
}
}
}
wraaped property
를 이니셜라이즈StateObject
로 선언된 해당 뷰 모델의 프로퍼티인 코인 데이터만을 CoinRowView
를 그리는 시점에서 건네받아 새롭게 이니셜라이즈하기 위함