Alexito's World

A world of coding 💻, by Alejandro Martinez

SwiftUI and Structured Concurrency

We're just in the week of WWDC and it's clear that Swift's Concurrency it's the biggest topic that will change how we develop. I've been following its evolution for months now and I'm happy to see how Apple talks about it on their sessions, it helps us see how much they really believe in this new langauge feature.

One of the things that I was not sure about is how the frameworks will integrate with certain aspects of the concurrency story. Things like how the UI frameworks would integrate with Global Actors were very clear, but how things would integrate with Structured Concurrency was still a question mark.

Lately I've been looking a lot into Kotlin's decisions and how they compare with Swift's. And one thing I'm was not sure is how nice it would be to work with "coroutine contexts" in Swift. Well now at least we have an answer on how SwiftUI is doing it.

Scopes

Up until now the best thing you could do was to start gathering the data for your view with .onAppear. That was a fine way of hooking into the view's lifecycle but once you have Structured Concurrency that's not enough.

To follow the rules of Structured Concurrency your async task must run in a defined scope. In small sample code that scope is defined statically by your source code.

func someAsyncFunction() async {
    await longRunningTask()
}

longRunningTask lives in the scope defined by someAsyncFunction and ties both functions together. The latter won't outlive the former. If the parent function is cancelled it will cancel the child and wait for it to finish. That's Structured Concurrency in a nutshell.

But in a context of a UI app, there is a runloop going. The scope is not as static as our source code. So, how can we attach tasks into a view lifecycle and ensure that when the view goes away all its linked structured tasks go away too?

SwiftUI lifecycle

SwiftUI introduces a new view modifier called .task. In its closure you can start async work that will be linked to the view's lifecycle.

struct DetailView: View {
    @State var list = Array(1...10)
    var body: some View {
        List(list, id: \.self) { n in
            Text("-> \(n)")
        }
        .task {
            await longOperation()
        }
    }
}

This means that the longOperation will be implicitly cancelled if the DetailView goes away. No need to manually manage its lifecycle, or store cancellables so ARC can do it for you. Nope! Structured Concurrency takes care of everything! 🎉

Other SwiftUI callbacks

One thing you need to be careful is that other callbacks like .refreshable don't seem to follow the rules of structured concurrency very well. In this case the refresh control will wait for your task to finish, but it won't cancel it if the view goes away.

I have an open radar about this: FB9139963

Conclusion

Structured Concurrency is a clear improvement to the current status quo of concurrent programming. It's not just about the nice async/await syntax, but about the structure that brings into asyncronous programming. I'm very happy to see SwiftUI offer some affordances for this style and I hope we see all frameworks do the same.

If you liked this article please consider supporting me