Getting Result out of a Task

1.7k views Asked by At

I have an asynchronous function that returns the result of an App Store query (was the query successful or not displayed in an alert). I am having trouble getting the result out of the task--Cannot find 'result' in scope. If I move let gotTitle = result ? "Success" : "The Request Failed" to within the task I see the warning "Initialization of immutable value 'gotTitle' was never used".

The Apple documentation on Task talks about using value or result to get data out of a task, but it isn't clear on how to do that. I have also stepped through the code with the debugger. It is showing correctly result = true and gotTitle = Success within the task.

struct TestAlert: View {

    var gotTitle: String = ""

    @State private var presentAlert = false

    var body: some View {

        VStack {

            Button(action: {
                Task {

                    let result = await restore()
                    print (result)
                    
                }
                let gotTitle = result ? "Success" : "The Request Failed"
                print(gotTitle)
            }) {
                Text("Restore Purchase")
            }
        }
        .alert(gotTitle, isPresented: $presentAlert, actions: {

        })

    }
    func restore() async -> Bool {

            return ((try? await AppStore.sync()) != nil)

    }
}
3

There are 3 answers

1
Rob On BEST ANSWER

You said:

I am having trouble getting the result out of the task

Cannot find 'result' in scope.

Yes, as presented in your question, result is a local variable inside the task, so you can't refer to it outside of the Task (unless you save it, or the title string, in a property of the struct).

If I move let gotTitle = result ? "Success" : "The Request Failed" to within the task I see the warning "Initialization of immutable value 'gotTitle' was never used".

Yes, you have defined gotTitle to be a local variable that just happens to have the same name as the property of the same name.


So, before I go to the solution, let's make a few observations so we understand what is going on. The key issue is that a Task with an await inside it runs asynchronously. Consider:

var body: some View {
    VStack {
        Button(action: {
            Task {
                print(Date(), "started task")
                let result = await restore()
                let gotTitle = result ? "Success" : "The Request Failed"  // a local variable; we'll fix this in the next example
                print(Date(), "task finished", gotTitle)
            }
            print(Date(), "after task submitted")
        }) {
            Text("Restore Purchase")
        }
    }
    .alert(gotTitle, isPresented: $presentAlert, actions: { })
}

Note that I have moved the let gotTitle inside the Task block. You can’t reference result outside of this block of code.

Anyway, when you tap on the button, you will see the following in the console:

2022-04-18 00:38:03 +0000 after task submitted
2022-04-18 00:38:03 +0000 started task
2022-04-18 00:38:06 +0000 task finished Success

Note the sequence of events:

  • “started task” showed up after “after task submitted”
  • “task finished” showed up seconds after “started task”

Hopefully, this illustrates why it makes no sense to refer to result where the printing of “after task submitted” is taking place. You haven’t even gotten to the declaration/assignment of result by that point.

So, the moral of the story is that if you want something updated after the asynchronous task, it needs to be immediately after the await line, inside the Task (or whatever context it is in). If you put it outside of the Task, it means that it won’t wait for the asynchronous Task to finish.


So, how would you access the result out of the Task block. You would save it to an ObservedProperty (which, coincidentally, helps separate the business logic from the view):

struct TestAlert: View {
    @ObservedObject var restoreRequest = AppStoreRestoreRequest()
    @State var isPresented = false

    var body: some View {
        VStack {
            Button {
                Task {
                    await restoreRequest.restore()
                    isPresented = true
                }
            } label: {
                Text("Restore Purchase")
            }
        }
        .alert(restoreRequest.state.title, isPresented: $isPresented, actions: { })
    }
}

class AppStoreRestoreRequest: ObservableObject {
    @Published var state: State = .notDetermined

    func restore() async {
        let result = (try? await AppStore.sync()) != nil
        state = result ? .success : .failure
    }
}

extension AppStoreRestoreRequest {
    enum State {
        case notDetermined
        case success
        case failure
    }
}

extension AppStoreRestoreRequest.State {
    var title: String {
        switch self {
        case .notDetermined: return "Not determined."
        case .success:       return "Success."
        case .failure:       return "The request failed."
        }
    }
}

So, the performance of the request (and the current state) is pulled out of the view and the state of the request is captured in the AppStoreRestoreRequest.

1
Jon Shier On

Though it's not recommended you combine view and business logic like this (SwiftUI prefers you put that behind an ObservableObject of some kind), you can use the Task just like a completion handler by putting your completion work inside it.

Task { @MainActor in // SwiftUI requires state is updated on the main queue.
  // Perform async work.
  // Update state.
}
0
Asperi On

Here is fixed variant

struct TestAlert: View {

    @State var gotTitle: String = ""          // << state !!
    @State private var presentAlert = false

    var body: some View {

        VStack {

            Button(action: {
                Task {

                    let result = await restore()
                    print (result)
                    await MainActor.run {     // << update on Main !!
                        gotTitle = result ? "Success" : "The Request Failed"
                        print(gotTitle)
                    }
                }
            }) {
                Text("Restore Purchase")
            }
        }
        .alert(gotTitle, isPresented: $presentAlert, actions: {

        })

    }
    func restore() async -> Bool {
        return ((try? await AppStore.sync()) != nil)
    }
}