Crash when triggering sheet presentation from a task

Given the following code, if line 17 isn't present there'll be a crash upon presenting the sheet.

Since content shouldn't be nil at the time of presentation, why does this crash?

import SwiftUI

struct ContentView: View {
    @State private var isPresented = false
    
    @State private var content: String?
    
    @State private var startTask = false
    
    var body: some View {
        Text("Tap me")
            .onTapGesture {
                startTask = true
            }
            .task(id: startTask) {
                guard startTask else { return }
                startTask = false  // <===== Crashes if removed
                content = "Some message"
                isPresented = true
            }
            .sheet(isPresented: $isPresented) {
                Text(content!)
            }
    }
}

iOS 16.4, Xcode 14.3.1

Accepted Reply

The question is answered in this post. The correct solution is to use capture lists.

Replies

It is my understanding that .task executes asynchronously, so it most likely isn't done when sheet executes.

Never mind, I just realized you have a state value that should logically mean the content is present.

I was poking around with it in a playground and it feels like a race condition is happening. It was working for a bit with the line commented out with some print statements added to try to see what was happening. Body is evaluated three times as would be expected.

I think I know the problem. You are accessing the content property value as it is at that moment, but you access the isPresented property in what amounts to the will set call, so you are seeing the future presented value, but the present content. I'm not sure of a fix for it since it becomes immutable if you remove the state wrapper.

I found a solution

import PlaygroundSupport
import SwiftUI

struct SheetView: View {
    @Binding var content: String?

    init(_ content: Binding<String?>) {
        _content = content
    }

    var body: some View {
        Text(content!)
    }
}

struct ContentView: View {
    @State private var isPresented = false

    @State private var content: String?

    @State private var startTask = false

    var body: some View {
        Text("Tap me")
            .onTapGesture {
                startTask = true
            }
            .frame(width: 500.0, height: 500.0)
            .task(id: startTask) {
                guard startTask else {
//                     startTask = false  // <===== Crashes if removed
                content = "Some message"
                isPresented = true
            }
            .sheet(isPresented: $isPresented) {
                SheetView($content)
            }
    }
}


let view = ContentView()
PlaygroundPage.current.setLiveView(view)

The question is answered in this post. The correct solution is to use capture lists.