66//
77
88import SwiftUI
9+ import async_task
910
1011fileprivate typealias ImageSize = OpenAIImageSize
12+ fileprivate typealias TaskModel = Async . SingleTask < Image , AsyncImageErrors >
1113
1214/// Async image component to load and show OpenAI image from OpenAI image API
1315@available ( iOS 15 . 0 , macOS 12 . 0 , tvOS 15 . 0 , watchOS 8 . 0 , * )
1416public struct OpenAIAsyncImage < Content: View , T: IOpenAILoader > : View {
1517
18+ @StateObject private var taskModel = TaskModel ( errorMapper: errorMapper)
19+
1620 /// Custom view builder template type alias
1721 public typealias ImageProcess = ( ImageState ) -> Content
1822
1923 /// Default loader, injected from environment
2024 @Environment ( \. openAIDefaultLoader) var defaultLoader : OpenAIDefaultLoader
21-
22- // MARK: - Private properties
23-
24- /// State variable to hold the OpenAI image
25- @State private var image : Image ?
26-
27- /// State variable to hold any errors encountered during loading
28- @State private var error : Error ?
29-
30- /// State variable to hold the current task responsible for loading the image
31- @State private var task : Task < Void , Never > ?
32-
25+
3326 // MARK: - Config
3427
3528 /// A binding to the text prompt describing the desired image. The maximum length is 1000 characters
@@ -85,15 +78,13 @@ public struct OpenAIAsyncImage<Content: View, T: IOpenAILoader>: View {
8578 }
8679 }
8780 . onChange ( of: prompt) { _ in
88- cancelTask ( )
89- clear ( )
90- task = getTask ( )
81+ start ( )
9182 }
9283 . onAppear {
93- task = getTask ( )
84+ start ( )
9485 }
9586 . onDisappear {
96- cancelTask ( )
87+ cancel ( )
9788 }
9889 }
9990
@@ -102,8 +93,8 @@ public struct OpenAIAsyncImage<Content: View, T: IOpenAILoader>: View {
10293 /// - Returns: The current image state status
10394 private func getState ( ) -> ImageState {
10495
105- if let image { return . loaded( image) }
106- else if let error { return . loadError( error) }
96+ if let image = taskModel . value { return . loaded( image) }
97+ else if let error = taskModel . error { return . loadError( error) }
10798
10899 return . loading
109100 }
@@ -139,42 +130,19 @@ public struct OpenAIAsyncImage<Content: View, T: IOpenAILoader>: View {
139130 }
140131 return try await loadImageDefault ( prompt, with: size, model: model)
141132 }
142-
143- /// Sets the image on the main thread
144- /// - Parameter value: The image to be set
145- @MainActor
146- private func setImage( _ value : Image ) {
147- image = value
148- }
149-
150- /// Clears the image and error state properties
151- @MainActor
152- private func clear( ) {
153- image = nil
154- error = nil
155- }
156-
157- /// Cancels the current loading task if any
158- private func cancelTask( ) {
159- task? . cancel ( )
160- task = nil
161- }
162-
133+
163134 /// Creates and returns a task to fetch the OpenAI image
164135 /// - Returns: A task that fetches the OpenAI image
165- private func getTask( ) -> Task < Void , Never > {
166- Task {
167- do {
168- if let image = try await loadImage ( prompt, with: size, model: model) {
169- setImage ( image)
170- }
171- } catch is CancellationError {
172- self . error = AsyncImageErrors . cancellationError
173- } catch{
174- self . error = error
175- }
136+ private func start( ) {
137+ taskModel. start {
138+ try await loadImage ( prompt, with: size, model: model)
176139 }
177140 }
141+
142+ /// Cancel task
143+ private func cancel( ) {
144+ taskModel. cancel ( )
145+ }
178146}
179147
180148// MARK: - Public extensions -
@@ -232,3 +200,12 @@ fileprivate func imageTpl(_ state : ImageState) -> some View{
232200 case . loading : ProgressView ( )
233201 }
234202}
203+
204+ @Sendable
205+ fileprivate func errorMapper( _ error : Error ? ) -> AsyncImageErrors ? {
206+ if error is CancellationError {
207+ return . cancellationError
208+ }
209+
210+ return nil
211+ }
0 commit comments