In view controller, user can select images, then app will upload to server.
User can select multiples images at one, and can select multiple times.
Each image uploaded will returns an URL to download later.
There is a Submit button to save other info and URLs of uploaded images to server.
I want when user press Submit button, app will wait for all images uploaded (returns URL or throws an error).
I'm using await Taskgroup to upload images in parallel each time user select images.
Is there any way to wait for uploading tasks finish other than using sleep()?
Update:
My code to upload images:
let urls = try await upload(images: images, urls: urls)
func upload(images: [UIImage], urls: [String]) async throws -> [String] {
try await withThrowingTaskGroup(of: String.self, returning: [String].self) { group in
for (index, image) in images.enumerated() {
group.addTask { [self] in
guard let url = try? await upload(image: image, to: urls[index]) else {
return ""
}
return url
}
}
var downloadURLs = [String]()
for try await result in group {
if result != "" {
downloadURLs.append(result)
}
}
return downloadURLs
}
}
Uploading images and submitting data is 2 difference actions:
- User select images, after that app will upload them automatically.
- User can tap Submit button whenever they like, even when images being uploaded, app has to wait for uploading task finish before submit other data.
CodePudding user response:
I assuming your upload image function look like thís
func uploadImage(image: UIImage) async -> URL {
// some logic
}
And you want to make many upload image task running in parallel and wait to all upload task to be done. So i think you can do something like
Task {
submitButton.isEnable = false
async let firstImage = uploadImage(image: image1)
async let secondImage = uploadImage(image: image2)
async let thirdImage = uploadImage(image: image3)
let images = await [firstImage, secondImage, thirdImage]
submitButton.isEnable = true
}
CodePudding user response:
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
}
}
}