I have an images carousel that fetching heavy images from a few URLs and displaying asynchronously when user is going to the next page. And the problem is that user need to see a waiting wheel and wait to see the next image.
So the ideal case would be to eliminate the waiting wheel by loading the next image while user still seeing the fist image and I don't know how to do it properly.
If you know the solution please let me know, I will mean a lot to me.
Here's the code:
struct ContentView: View {
@State private var selectedImageToSee: Int = 0
private let imagesUrl: [String] = [
"https://images.unsplash.com/photo-1633125195723-04860dde4545?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzNDIyMDUyMQ&ixlib=rb-1.2.1&q=80&w=3024",
"https://images.unsplash.com/photo-1633090332452-532d6b39422a?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzNDIyMDI1Ng&ixlib=rb-1.2.1&q=80&w=3024",
"https://images.unsplash.com/photo-1591667954789-c25d5affec59?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNzU1&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024",
"https://images.unsplash.com/photo-1580316576539-aee1337e80e8?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNzk5&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024",
"https://images.unsplash.com/photo-1484976063837-eab657a7aca7?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNjky&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024"
]
var body: some View {
ImagesCaruselView(selectedImageToSee: $selectedImageToSee, urls: imagesUrl)
}
}
struct ImagesCaruselView: View {
@Binding var selectedImageToSee: Int
let urls: [String]
var body: some View {
TabView(selection: $selectedImageToSee) {
ForEach(0..<urls.count, id: \.self) { url in
AsyncImageView(
url: URL(string: urls[url])!,
placeholder: { ProgressView().scaleEffect(1.5).progressViewStyle(CircularProgressViewStyle(tint: .gray)) },
image: { Image(uiImage: $0) }
)
.padding()
}
}
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
struct AsyncImageView<Placeholder: View>: View {
@StateObject private var loader: ImageLoader
private let placeholder: Placeholder
private let image: (UIImage) -> Image
init(
url: URL,
@ViewBuilder placeholder: () -> Placeholder,
@ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:)
) {
self.placeholder = placeholder()
self.image = image
_loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue))
}
var body: some View {
content
.onAppear(perform: loader.load)
}
private var content: some View {
Group {
if loader.image != nil {
image(loader.image!)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
placeholder
}
}
}
}
protocol ImageCache {
subscript(_ url: URL) -> UIImage? { get set }
}
struct TemporaryImageCache: ImageCache {
private let cache: NSCache<NSURL, UIImage> = {
let cache = NSCache<NSURL, UIImage>()
cache.countLimit = 100 // 100 items
cache.totalCostLimit = 1024 * 1024 * 200 // 200 MB
return cache
}()
subscript(_ key: URL) -> UIImage? {
get { cache.object(forKey: key as NSURL) }
set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) }
}
}
import Combine
class ImageLoader: ObservableObject {
@Published var image: UIImage?
private(set) var isLoading = false
private let url: URL
private var cache: ImageCache?
private var cancellable: AnyCancellable?
private static let imageProcessingQueue = DispatchQueue(label: "image-processing")
init(url: URL, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancel()
}
func load() {
guard !isLoading else { return }
if let image = cache?[url] {
self.image = image
return
}
#warning("Caching the image are disabled")
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.handleEvents(receiveSubscription: { [weak self] _ in self?.onStart() },
// receiveOutput: { [weak self] in self?.cache($0) },
receiveCompletion: { [weak self] _ in self?.onFinish() },
receiveCancel: { [weak self] in self?.onFinish() })
.subscribe(on: Self.imageProcessingQueue)
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
func cancel() {
cancellable?.cancel()
}
private func onStart() {
isLoading = true
}
private func onFinish() {
isLoading = false
}
private func cache(_ image: UIImage?) {
image.map { cache?[url] = $0 }
}
}
import SwiftUI
struct ImageCacheKey: EnvironmentKey {
static let defaultValue: ImageCache = TemporaryImageCache()
}
extension EnvironmentValues {
var imageCache: ImageCache {
get { self[ImageCacheKey.self] }
set { self[ImageCacheKey.self] = newValue }
}
}