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
Comma Automatically Being Added to Textfield in Swift
Macos App Sandboxing - Read Access to Referenced Files from Parsed Xml
Cast While Looping Over Dictionary in Swift
Parse Weird Bug in Swift That Causes Acl Write Permissions to Change to an Objectid
How to Create Rounded Image with Border and Shadow as Mkannotationview in Swift
Filter, Closure, Functional Syntax Version of for Loop with Multiple Conditions
Reactive Cocoa/Reactive Swift - Swift 3.0 Missing Methods
Swift: Object Instance by Name
How to Get Unsaferawpointer on The Swift Object
How to Provide Default Implementation of an Objective-C Protocol in a Swift Protocol Extension
Underlying Type for Tuple in Swift
Uibutton Borders Function Only Gives Back White Borders
Why Is Swift Giving Me Inaccurate Floating Point Arithmetic Results
Swift Applications Takes More Space on Disk
Aurendercallbackstruct in Swift