Skip to content

Instantly share code, notes, and snippets.

@pwightman
Last active July 21, 2020 17:51
Show Gist options
  • Save pwightman/d6cf86beb434ab5a81efcb32bcdbf8cd to your computer and use it in GitHub Desktop.
Save pwightman/d6cf86beb434ab5a81efcb32bcdbf8cd to your computer and use it in GitHub Desktop.
Basic image fetcher, with in-memory cache and request deduplication.
import Combine
import SwiftUI
import UIKit
struct RemoteImage: View {
let url: URL
var placeholder: AnyView?
@StateObject var fetcher = RemoteImageFetcher()
func placeholder<Placeholder: View>(@ViewBuilder _ view: () -> Placeholder) -> RemoteImage {
var v = self
v.placeholder = AnyView(view())
return v
}
var body: some View {
// Using Group here never triggers onChange/onAppear, presumably because SwiftUI
// has some sort of optimization where Group { EmptyView } never renders? Putting
// it in a VStack fixes it.
VStack {
if let image = fetcher.image {
Image(uiImage: image)
.resizable()
.renderingMode(.original)
} else if let placeholder = placeholder {
placeholder
} else {
EmptyView()
}
}
.onAppear {
fetcher.url = url
}
.onChange(of: url) { value in
fetcher.url = value
}
}
}
class RemoteImageFetcher: ObservableObject {
@Published var image: UIImage?
@Published var url: URL?
init() {
$url
.removeDuplicates()
.map { url -> AnyPublisher<UIImage?, Error> in
if let url = url {
return RemoteImageStore.shared.load(url: url)
.eraseToAnyPublisher()
} else {
return Just(nil)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
.switchToLatest()
.replaceError(with: nil)
.assign(to: $image)
}
}
class RemoteImageStore {
static let shared = RemoteImageStore()
let queue = DispatchQueue(label: "com.myapp.RemoteImageStore", qos: .userInitiated)
private var subjects: [URL: CurrentValueSubject<UIImage?, Error>] = [:]
private var cancellables: [UUID: AnyCancellable] = [:]
func load(url: URL) -> AnyPublisher<UIImage?, Error> {
if let subject = subjects[url] {
return subject.eraseToAnyPublisher()
} else {
let subject = CurrentValueSubject<UIImage?, Error>(nil)
let uuid = UUID()
cancellables[uuid] = URLSession.shared.dataTaskPublisher(for: url)
.receive(on: queue)
.map(\.data)
.map { UIImage(data: $0) }
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.cancellables.removeValue(forKey: uuid)?.cancel()
} receiveValue: { [weak subject] image in
subject?.send(image)
}
subjects[url] = subject
return subject.eraseToAnyPublisher()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment