I'm trying to create a simple ImageDownloader
framework in swift.
What I'd like to achieve:
- Able to download images with given URLs
- Cache with url string
So fetching just one image is no problem, I just used func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
from URLSession
to get the data and pass it into UIImage
.
However, my question is, how should I change it into a framework that supports concurrent download many images at the same time?
Should I used DispatchQueue
and every time the task is created with an url, add that task into the queue? e.g.:
private let threadSafeDictQueue = DispatchQueue(label: "taskQueue", attributes: .concurrent)
public func fetchImageBy(iconUrl: String, defaultImageInfo: DefaultImageInfo? = nil, completion: @escaping (IconResponse) -> Void) {
// enqueue the request
queue.async { [weak self] in
let task = session.dataTask(with: url) { [weak self] data, urlResponse, error in
//code...}
}
}
OR should I consider OperationQueue? If so, how should I do it?
Thanks!
CodePudding user response:
No, it uses a closure to return a result because it already does this for you, you are not going to block on the call, you will trigger downloading, probable using a thread, and then when its finished it will call the result closure, because of this you need to be aware that your app state could have changed when you get your result.
CodePudding user response:
Answering "...my question is, how should I change it into a framework that supports concurrent download many images at the same time?". Here is my approach to address what you want to achieve, that is, "Able to download images with given URLs" and "Cache with url string". Similarly, "Should I add URLSession tasks inside concurrent dispatchQueue?", no, use this approach instead, it works well for me.
class ImageDownloader: ObservableObject, @unchecked Sendable {
@Published var images: [ImageModel] = []
func download(urls: [String]) async {
return await withTaskGroup(of: ImageModel.self) { group in
for str in urls {
if let url = URL(string: str) {
group.addTask { await self.downloadImage(url: url) }
}
}
for await result in group {
DispatchQueue.main.async {
self.images.append(result)
}
}
}
}
private func downloadImage(url: URL) async -> ImageModel {
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let img = UIImage(data: data) {
return ImageModel(url: url, image: img)
}
}
catch { print(error) }
return ImageModel()
}
}
struct ImageModel: Identifiable, Hashable, Sendable {
let id = UUID().uuidString
var url: URL?
var image: UIImage?
}
extension URL: @unchecked Sendable { }
extension UIImage: @unchecked Sendable { }
Use it like this:
Task {
await ImageDownloader().download(urls: urls)
}
or, for example in SwiftUI,
struct ContentView: View {
@StateObject var downloader = ImageDownloader()
let urls: [String] = [
"https://upload.wikimedia.org/wikipedia/commons/b/b2/Euphorbia_candelabrum_002.JPG",
"https://upload.wikimedia.org/wikipedia/commons/b/b2/Euphorbia_candelabrum_003.JPG",
"https://upload.wikimedia.org/wikipedia/commons/0/0d/Euphorbia_candelabrum_005.jpg"
]
var body: some View {
ScrollView {
VStack (spacing: 10) {
ForEach(downloader.images) { photo in
Image(uiImage: photo.image ?? UIImage())
.resizable()
.frame(width: 222, height: 222)
.aspectRatio(contentMode: .fill)
}
}
}
.task {
await downloader.download(urls: urls)
}
}
}