[SwiftUI] CryptoApp: Image Download

Junyoung Park·2022년 11월 3일
0

SwiftUI

목록 보기
77/136
post-thumbnail

Downloading Coin Images using Combine, MVVM, and a Networking Layer | SwiftUI Crypto App #8

CryptoApp: Image Download

구현 목표

  • 비동기 이미지 다운로드를 통한 UI 패치

구현 태스크

  • 이미지 다운로드 데이터 서비스 클래스 구현
  • 데이터 서비스를 사용하는 뷰 모델 구현
  • 뷰 단의 뷰 모델 이니셜라이즈

핵심 코드

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()
            })
    }
  • 뷰 모델이 데이터 서비스를 사용할 때 이니셜라이즈 단에서 건넨 코인 데이터의 URL 정보를 통해 이미지 패치
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를 그리는 시점에서 건네받아 새롭게 이니셜라이즈하기 위함

구현 화면

profile
JUST DO IT

0개의 댓글