Hello! I have been trying to asynchronously load an image in my app using combine. Currently all the other pieces of data are loading fine, but my image seem to be stuck in a progress view. Why?
This is my code:
import SwiftUI
struct ApodView: View {
@StateObject var vm = ApodViewModel()
var body: some View {
ZStack {
// Background Layer
Color.theme.background
.ignoresSafeArea()
// Content Layer
VStack() {
Text(vm.apodData?.title ?? "Placeholder")
.font(.title)
.fontWeight(.bold)
.multilineTextAlignment(.center)
.foregroundColor(Color.theme.accent)
ApodImageView(apodData: vm.apodData ?? ApodModel(date: "", explanation: "", url: "", thumbnailUrl: "", title: ""))
ZStack() {
Color.theme.backgroundTextColor
ScrollView(showsIndicators: false) {
Text(vm.apodData?.explanation ?? "Loading...")
.font(.body)
.fontWeight(.semibold)
.foregroundColor(Color.theme.accent)
.multilineTextAlignment(.center)
.padding()
}
}
.cornerRadius(10)
}
.padding()
}
}
}
import SwiftUI
struct ApodImageView: View {
@StateObject var vm: ApodImageViewModel
init(apodData: ApodModel) {
_vm = StateObject(wrappedValue: ApodImageViewModel(apodData: apodData))
}
var body: some View {
ZStack {
if let image = vm.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
} else if vm.isLoading {
ProgressView()
} else {
Image(systemName: "questionmark")
.foregroundColor(Color.theme.secondaryText)
}
}
.frame(maxWidth: .infinity, maxHeight: 250)
.cornerRadius(10)
}
}
import Foundation
import SwiftUI
import Combine
class ApodImageViewModel: ObservableObject {
@Published var image: UIImage?
@Published var isLoading: Bool = false
private let apodData: ApodModel
private let dataService: ApodImageService
private var cancellables = Set<AnyCancellable>()
init(apodData: ApodModel) {
self.apodData = apodData
self.dataService = ApodImageService(apodData: apodData)
self.addSubscribers()
self.isLoading = true
}
private func addSubscribers() {
dataService.$image
.sink { [weak self] _ in
self?.isLoading = false
} receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
}
.store(in: &cancellables)
}
}
import Foundation
import SwiftUI
import Combine
class ApodImageService: ObservableObject {
@Published var image: UIImage?
private var imageSubscription: AnyCancellable?
private let apodData: ApodModel
init(apodData: ApodModel) {
self.apodData = apodData
getApodImage()
}
private func getApodImage() {
guard let url = URL(string: apodData.thumbnailUrl ?? apodData.url) else { return }
imageSubscription = NetworkingManager.download(url: url)
.tryMap({ data -> UIImage? in
return UIImage(data: data)
})
.sink(receiveCompletion: NetworkingManager.handleCompletion, receiveValue: { [weak self] returnedImage in
self?.image = returnedImage
self?.imageSubscription?.cancel()
})
}
}
import Foundation
import Combine
class NetworkingManager {
enum NetworkingError: LocalizedError {
case badURLResponse(url: URL)
case unknown
var errorDescription: String? {
switch self {
case .badURLResponse(url: let url): return "Bad response from URL: \(url)"
case .unknown: return "Unknown Error Returned"
}
}
}
static func download(url: URL) -> AnyPublisher<Data, Error> {
return URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap({ try handleURLResponse(output: $0, url: url) })
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
static func handleURLResponse(output: URLSession.DataTaskPublisher.Output, url: URL) throws -> Data {
guard let response = output.response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw NetworkingError.badURLResponse(url: url)
}
return output.data
}
static func handleCompletion(completion: Subscribers.Completion<Error>) {
switch completion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
}
}