From 259ca98d5d47d13882ba04ee00df2c3c8faaa1dd Mon Sep 17 00:00:00 2001 From: Jonathan Avila Date: Sun, 24 Mar 2024 14:33:49 -0600 Subject: [PATCH] Display single alert and/or success message when upscaling in batch --- Superres/Utilities/ImageState.swift | 2 +- Superres/Views/ContentView.swift | 6 +- Superres/Views/ContentViewModel.swift | 115 ++++++++++++++++---------- 3 files changed, 77 insertions(+), 46 deletions(-) diff --git a/Superres/Utilities/ImageState.swift b/Superres/Utilities/ImageState.swift index 514eed1..07304a2 100644 --- a/Superres/Utilities/ImageState.swift +++ b/Superres/Utilities/ImageState.swift @@ -48,7 +48,7 @@ struct ImageState: Identifiable { } } - func saveUpscaledImage(to url: URL) throws { + private func saveUpscaledImage(to url: URL) throws { guard let upscaledImage = upscaledImage else { return } diff --git a/Superres/Views/ContentView.swift b/Superres/Views/ContentView.swift index f8c5302..64548d5 100644 --- a/Superres/Views/ContentView.swift +++ b/Superres/Views/ContentView.swift @@ -43,7 +43,9 @@ struct ContentView: View { Spacer() Button("Upscale") { - viewModel.upscaleImages() + Task { + await viewModel.upscaleImages() + } } .buttonStyle(CustomButtonStyle(isProminent: true, useMaxWidth: true)) @@ -52,7 +54,7 @@ struct ContentView: View { .padding() .frame(width: 200, alignment: .leading) .background(Color("BgColor")) - .disabled(viewModel.isWorking) + .disabled(viewModel.isUpscaling) DividerView() diff --git a/Superres/Views/ContentViewModel.swift b/Superres/Views/ContentViewModel.swift index bd98edc..13d0318 100644 --- a/Superres/Views/ContentViewModel.swift +++ b/Superres/Views/ContentViewModel.swift @@ -14,7 +14,7 @@ final class ContentViewModel: ObservableObject { @Published var alertMessage = "" @Published var automaticallySave = false @Published var showSuccessMessage = false - @Published var isWorking = false + @Published var isUpscaling = false @Published var imagesNeedUpscaling: Bool = false private var outputFolderUrl: URL? @@ -30,51 +30,67 @@ final class ContentViewModel: ObservableObject { let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path return url.path.replacingOccurrences(of: homeDirectory, with: "~") } - - func upscaleImages() { - isWorking = true - - for index in imageStates.indices where imageStates[index].upscaledImage == nil { - self.imageStates[index].isUpscaling = true - } - - Task { - let saveImageSuccess = await withTaskGroup(of: Bool.self) { group -> Bool in - for index in self.imageStates.indices where self.imageStates[index].upscaledImage == nil { - group.addTask { - do { - let upscaledImageData = try await upscale(self.imageStates[index].originalImageUrl) - try await MainActor.run { - self.imageStates[index].isUpscaling = false - self.imageStates[index].upscaledImage = NSImage(data: upscaledImageData!) - if self.automaticallySave, let outputFolderUrl = self.outputFolderUrl { - try self.imageStates[index].saveUpscaledImageToFolder(folderUrl: outputFolderUrl) - } - } - return true // Image was saved. - - } catch { - await MainActor.run { - self.imageStates[index].isUpscaling = false - self.displayAlert(title: "Error", message: error.localizedDescription) - } + + /// Upscale images asynchronously. + @MainActor + func upscaleImages() async { + isUpscaling = true + + var taskResults: [(String?, Bool)] = [] + + // Create a task group to execute upscaling tasks concurrently. The task result is a tuple of an optional string (an error description if upscaling fails) and a boolean (whether the upscaled image was automatically saved). + await withTaskGroup(of: (String?, Bool).self) { group in + + // Process images that have not been upscaled. + for index in imageStates.indices where imageStates[index].upscaledImage == nil { + group.addTask { + do { + await MainActor.run { + self.imageStates[index].isUpscaling = true + } + + let upscaledImageData = try await upscale(self.imageStates[index].originalImageUrl) + + await MainActor.run { + self.imageStates[index].upscaledImage = NSImage(data: upscaledImageData!) + self.imageStates[index].isUpscaling = false } - return false // Image was not saved. + + if self.automaticallySave, let outputFolderUrl = self.outputFolderUrl { + try self.imageStates[index].saveUpscaledImageToFolder(folderUrl: outputFolderUrl) + return (nil, true) + } + + return (nil, false) + } catch { + await MainActor.run { + self.imageStates[index].isUpscaling = false + } + return ("Error upscaling image \(self.imageStates[index].originalImageUrl): \(error.localizedDescription)", false) } } - return await group.contains(true) } - - if saveImageSuccess { - await MainActor.run { - triggerSuccessMessage() - } - } - - await MainActor.run { - self.isWorking = false + + // Collect results once all tasks have completed. + for await result in group { + taskResults.append(result) } } + + isUpscaling = false + + // Display any error messages in a single alert. + let errorMessages = taskResults.compactMap { $0.0 } + if !errorMessages.isEmpty { + let errorMessage = errorMessages.joined(separator: "\n") + displayAlert(title: "Error", message: errorMessage) + } + + // Display success message if any upscaled images where saved. + let imageWasSaved = taskResults.contains { $0.1 } + if imageWasSaved { + triggerSuccessMessage() + } } private func displayAlert(title: String, message: String) { @@ -93,18 +109,27 @@ final class ContentViewModel: ObservableObject { } } + /// Handles the drop of images onto the application. + /// - Parameter providers: An array of `NSItemProvider` objects representing the dropped items. + /// - Returns: `true` if the drop was handled successfully, `false` otherwise. func handleDropOfImages(providers: [NSItemProvider]) -> Bool { var errorMessages = [String]() for provider in providers { if provider.canLoadObject(ofClass: URL.self) { - _ = provider.loadObject(ofClass: URL.self) { url, _ in + _ = provider.loadObject(ofClass: URL.self) { url, error in DispatchQueue.main.async { + if let error = error { + errorMessages.append("Error loading URL: \(error.localizedDescription)") + return + } + guard let url = url else { return } + // Get the UTType from the URL. guard let typeIdentifier = try? url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier, let fileUTType = UTType(typeIdentifier) else { @@ -112,11 +137,13 @@ final class ContentViewModel: ObservableObject { return } + // Check if the file type is supported. if !ImageState.supportedImageTypes.contains(fileUTType) { - errorMessages.append("Unsupported image type: \(url.path())") + errorMessages.append("Unsupported file type: \(url.path())") return } + // Load the image from the URL. guard let nsImage = NSImage(contentsOf: url) else { errorMessages.append("Unable to load image: \(url.path())") return @@ -130,9 +157,11 @@ final class ContentViewModel: ObservableObject { } DispatchQueue.main.async { + // Display any error messages in a single alert. if !errorMessages.isEmpty { + let title = "Error loading image\(errorMessages.count > 1 ? "s" : "")" let message = errorMessages.joined(separator: "\n") - self.displayAlert(title: "Error", message: message) + self.displayAlert(title: title, message: message) } }