Loading Image from Remote Url Asynchronously in Swiftui Image Using Combine's Publisher

Loading image from remote URL asynchronously in SwiftUI Image using combine's Publisher

You have to use an ObservableObject for subscribing to the publisher provided by ImageLoader.

class ImageProvider: ObservableObject {
@Published var image = UIImage(named: "icHamburger")!
private var cancellable: AnyCancellable?
private let imageLoader = ImageLoader()

func loadImage(url: URL) {
self.cancellable = imageLoader.publisher(for: url)
.sink(receiveCompletion: { failure in
print(failure)
}, receiveValue: { image in
self.image = image
})
}
}

struct MyImageView: View {
var url: URL
@StateObject var viewModel = ImageProvider()
var body: some View {
Image(uiImage: viewModel.image)
.onAppear {
viewModel.loadImage(url: url)
}
}
}

How to display Image from a url in SwiftUI

iOS 15 update:

you can use asyncImage in this way:
AsyncImage(url: URL(string: "https://your_image_url_address"))

more info on Apple developers document:
AsyncImage

Using ObservableObject (Before iOS 15)

first you need to fetch image from url :

class ImageLoader: ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}

init(urlString:String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
}
}
task.resume()
}
}

you can put this as a part of your Webservice class function too.

then in your ContentView struct you can set @State image in this way :

struct ImageView: View {
@ObservedObject var imageLoader:ImageLoader
@State var image:UIImage = UIImage()

init(withURL url:String) {
imageLoader = ImageLoader(urlString:url)
}

var body: some View {

Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:100, height:100)
.onReceive(imageLoader.didChange) { data in
self.image = UIImage(data: data) ?? UIImage()
}
}
}

Also, this tutorial is a good reference if you need more

SwiftUI Image from URL not showing

Try using @Published - then you don't need a custom PassthroughSubject:

class ImageLoader: ObservableObject {
// var didChange = PassthroughSubject<Data, Never>() <- remove this
@Published var data: Data?
...
}

and use it in your view:

struct ImageView: View {
var urlString: String
@ObservedObject var imageLoader = ImageLoader()
@State var image = UIImage(named: "homelessDogsCats")!

var body: some View {
ZStack() {
Image(uiImage: image)
.resizable()
.onReceive(imageLoader.$data) { data in
guard let data = data else { return }
self.image = UIImage(data: data) ?? UIImage()
}
}.onAppear {
self.imageLoader.loadData(from: urlString)
}
}
}

Note: if you're using SwiftUI 2, you can use @StateObject instead of @ObservedObject and onChange instead of onReceive.

struct ImageView: View {
var urlString: String
@StateObject var imageLoader = ImageLoader()
@State var image = UIImage(named: "homelessDogsCats")!

var body: some View {
ZStack() {
Image(uiImage: image)
.resizable()
.onChange(of: imageLoader.data) { data in
guard let data = data else { return }
self.image = UIImage(data: data) ?? UIImage()
}
}.onAppear {
self.imageLoader.loadData(from: urlString)
}
}
}

Updating image view in SwiftUI after downloading image

The problem seems to be related to some kind of lost synchronization between the ContentView and the ImageURL (that happens after the button click event).

A possible workaround is making the ImageURL a @State property of the ContentView.

After that, inside the scope of the button click event, we can call the image.imageLoader.load(url: ) method. As the download of the image ends, the publisher (didChange) will notify the ImageURL and then the change is correctly propagated to the ContentView.

import SwiftUI
import Combine

enum ImageURLError: Error {
case dataIsNotAnImage
}

class ImageLoader: BindableObject {
/*
init(url: URL) {
self.url = url
}

private let url: URL */

let id: String = UUID().uuidString
var didChange = PassthroughSubject<Void, Never>()

var image: UIImage? {
didSet {
DispatchQueue.main.async {
self.didChange.send()
}
}
}

func load(url: URL) {
print(#function)
self.image = nil

URLSession.shared.dataTask(with: url) { (data, res, error) in
guard error == nil else {
return
}

guard
let data = data,
let image = UIImage(data: data)
else {
return
}

self.image = image
}.resume()
}

}

URLImage view:

struct URLImage : View {

init() {
self.placeholder = Image(systemName: "photo")
self.imageLoader = ImageLoader()
}

@ObjectBinding var imageLoader: ImageLoader

var placeholder: Image

var body: some View {
imageLoader.image == nil ?
placeholder : Image(uiImage: imageLoader.image!)
}
}

ContentView:

struct ContentView : View {

@State var url: String = "https://robohash.org/random.png"
@State var image: URLImage = URLImage()

var body: some View {
VStack {
Button(action: {
self.url = "https://robohash.org/\(Int.random(in: 0 ..< 10)).png"
self.image.imageLoader.load(url: URL(string: self.url)!)
}) {
Text("Get Random Robot Image")
}

image
}
}
}

Anyway I will try to investigate the problem and if I will know something new I will modify my answer.

Swift synchronous function waiting for asynchronous functions to finish

What about creating a dictionary [URL: UIImage] in your class to keep the state of the uploads (so add/remove when starting/finishing each) and enable your submit button only when that is empty? Do mind wrapping it into a class with a thread-safe implementation though.

EDIT: If the button can't be disabled, you could do something like below:

import SwiftUI
import Combine

struct ContentView: View {
let viewModel = ViewModel()

var body: some View {
VStack {
Button {
self.viewModel.upload(url: URL(string: "https://google.com?id=\(UUID().uuidString)")!, image: UIImage())
self.viewModel.upload(url: URL(string: "https://google.com?id=\(UUID().uuidString)")!, image: UIImage())
}
label: {
Rectangle()
.fill(Color.red)
}

Button {
self.viewModel.submit()
}
label: {
Rectangle()
.fill(Color.blue)
}
}
.padding()
}
}

class ViewModel: ObservableObject {
var uploadRequests = [URL: UIImage]()
var submitting = false

let requestPublisher = PassthroughSubject<Bool, Never>()
var cancellable: AnyCancellable?

init() {
self.cancellable = requestPublisher.filter { [weak self] _ in
self?.submitting ?? false
}.sink { [weak self] _ in
self?.uploadToServer()
}
}

func upload(url: URL, image: UIImage) {
Task {
print("Uploading image")

self.uploadRequests[url] = image

try await Task.sleep(nanoseconds: 1_000_000_000)

self.uploadRequests[url] = nil

print("Image uploaded")

// If submit button was pressed meanwhile...
if self.uploadRequests.count == 0 {
self.requestPublisher.send(true)
}
}
}

func submit() {
self.submitting = true

guard self.uploadRequests.count == 0 else {
print("Still ongoing requests")
return
}

self.uploadToServer()
}

func uploadToServer() {
Task { [weak self] in
guard let self = self else { return }

print("Uploading to server")

self.submitting = false
}
}
}

Combine framework: how to process each element of array asynchronously before proceeding

With your latest edit and this comment below:

I literally am asking is there a Combine equivalent of "don't proceed to the next step until this step, involving multiple asynchronous steps, has finished"

I think this pattern can be achieved with .flatMap to an array publisher (Publishers.Sequence), which emits one-by-one and completes, followed by whatever per-element async processing is needed, and finalized with a .collect, which waits for all elements to complete before proceeding

So, in code, assuming we have these functions:

func getFoos() -> AnyPublisher<[Foo], Error>
func getPartials(for: Foo) -> AnyPublisher<[Partial], Error>
func getMoreInfo(for: Partial, of: Foo) -> AnyPublisher<MoreInfo, Error>

We can do the following:

getFoos()
.flatMap { fooArr in
fooArr.publisher.setFailureType(to: Error.self)
}

// per-foo element async processing
.flatMap { foo in

getPartials(for: foo)
.flatMap { partialArr in
partialArr.publisher.setFailureType(to: Error.self)
}

// per-partial of foo async processing
.flatMap { partial in

getMoreInfo(for: partial, of: foo)
// build completed partial with more info
.map { moreInfo in
var newPartial = partial
newPartial.moreInfo = moreInfo
return newPartial
}
}
.collect()
// build completed foo with all partials
.map { partialArr in
var newFoo = foo
newFoo.partials = partialArr
return newFoo
}
}
.collect()

(Deleted the old answer)



Related Topics



Leave a reply



Submit