3

I have an app that does some processing given a string, this is done in 2 Tasks. During this time i'm displaying an animation. When these Tasks complete i need to hide the animation. The below code works, but is not very nice to look at. I believe there is a better way to do this?

let firTask = Task {
    /* Slow-running code */
}

let airportTask = Task {
    /* Even more slow-running code */
}

Task {
    _ = await firTask.result
    _ = await airportTask.result
    
    self.isVerifyingRoute = false
}
George
  • 25,988
  • 10
  • 79
  • 133
Maciej Swic
  • 11,139
  • 8
  • 52
  • 68

2 Answers2

8

Isn't the real problem that this is a misuse of Task? A Task, as you've discovered, is not really of itself a thing you can await. If the goal is to run slow code in the background, use an actor. Then you can cleanly call an actor method with await.

let myActor = MyActor()
await myActor.doFirStuff()
await myActor.doAirportStuff()
self.isVerifyingRoute = false

However, we also need to make sure we're on the main thread when we talk to self — something that your code omits to do. Here's an example:

actor MyActor {
    func doFirStuff() async {
        print("starting", #function)
        await Task.sleep(2 * 1_000_000_000)
        print("finished", #function)
    }
    func doAirportStuff() async {
        print("starting", #function)
        await Task.sleep(2 * 1_000_000_000)
        print("finished", #function)
    }
}
func test() {
    let myActor = MyActor()
    Task {
        await myActor.doFirStuff()
        await myActor.doAirportStuff()
        Task { @MainActor in
            self.isVerifyingRoute = false
        }
    }
}

Everything happens in the right mode: the time-consuming stuff happens on background threads, and the call to self happens on the main thread. A cleaner-looking way to take care of the main thread call, in my opinion, would be to have a @MainActor method:

func test() {
    let myActor = MyActor()
    Task {
        await myActor.doFirStuff()
        await myActor.doAirportStuff()
        self.finish()
    }
}
@MainActor func finish() {
    self.isVerifyingRoute = false
}

I regard that as elegant and clear.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    For an example, see my https://www.biteinteractive.com/swift-5-5-replacing-gcd-with-async-await/ — see the section An Actor Prepares. – matt Aug 10 '21 at 12:47
  • 1
    (Another problem with your code is that you seem to be saying `self.isVerifyingRoute = false` on a background thread. That could be a bad idea.) – matt Aug 10 '21 at 12:53
  • Really good linked article! I did notice further down, the old `async { ... }` code blocks, which should probably be updated with `Task { ... }`. Good read, recommend! – George Aug 10 '21 at 14:46
  • @George You're absolutely right about that change, I need to go back and update the article. – matt Aug 10 '21 at 14:50
  • 1
    Nice. Also just as an extra note, I gave this question the `swift-concurrency` tag, because there currently isn't a tag for this for Swift (just using `concurrency` on it's own isn't very specific). Feel free to peer review it & make any changes, so this tag can be used. – George Aug 10 '21 at 14:58
  • Oooo that new tag is a great idea. – matt Aug 10 '21 at 15:00
4

I would make the tasks discardable with an extension. Perhaps something like this:

extension Task {
    @discardableResult
    func finish() async -> Result<Success, Failure>  {
        await self.result
    }
}

Then you could change your loading task to:

Task {
    defer { self.isVerifyingRoute = false }
    await firTask.finish()
    await airportTask.finish()
}
Jake
  • 2,126
  • 1
  • 10
  • 23