How to load remote image in SwiftUI

Issue #448

Use ObservableObject and onReceive to receive event. URLSession.dataTask reports in background queue, so need to .receive(on: RunLoop.main) to receive events on main queue.

For better dependency injection, need to use ImageLoader from Environment

There should be a way to propagate event from Publisher to another Publisher, for now we use sink

ImageLoader.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Combine
import WatchKit

class ImageLoader: ObservableObject {
private var cancellable: AnyCancellable?
let objectWillChange = PassthroughSubject<UIImage?, Never>()

func load(url: URL) {
self.cancellable = URLSession.shared
.dataTaskPublisher(for: url)
.map({ $0.data })
.eraseToAnyPublisher()
.receive(on: RunLoop.main)
.map({ UIImage(data: $0) })
.replaceError(with: nil)
.sink(receiveValue: { image in
self.objectWillChange.send(image)
})
}

func cancel() {
cancellable?.cancel()
}
}

RemoteImage.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import SwiftUI
import WatchKit

struct RemoteImage: View {
let url: URL
let imageLoader = ImageLoader()
@State var image: UIImage? = nil

var body: some View {
Group {
makeContent()
}
.onReceive(imageLoader.objectWillChange, perform: { image in
self.image = image
})
.onAppear(perform: {
self.imageLoader.load(url: self.url)
})
.onDisappear(perform: {
self.imageLoader.cancel()
})
}

private func makeContent() -> some View {
if let image = image {
return AnyView(
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
)
} else {
return AnyView(Text("😢"))
}
}
}

Comments