2

I have a simple view that is using a class to generate a link for the user to share.

This link is generated asynchronously so is run by using the .task modifier.

class SomeClass : ObservableObject {

func getLinkURL() async -> URL {
    
    try? await Task.sleep(for: .seconds(1))
    return URL(string:"https://www.apple.com")!
  }
}

struct ContentView: View {

@State var showSheet = false
@State var link : URL?
@StateObject var someClass = SomeClass()

var body: some View {
    VStack {
        Button ("Show Sheet") {
            showSheet.toggle()
        }
    }
    .padding()
    .sheet(isPresented: $showSheet) {
        if let link = link {
            ShareLink(item: link)
        } else {
            HStack {
                ProgressView()
                Text("Generating Link")
            }
            
        }
    }.task {
        let link = await someClass.getLinkURL()
        print ("I got the link",link)
        await MainActor.run {
            self.link = link
        }
    }
    
   }
}

I've simplified my actual code to this example which still displays the same behavior. The task is properly executed when the view appears, and I see the debug print for the link. But when pressing the button to present the sheet the link is nil.

enter image description here

The workaround I found for this is to move the .task modifier to be inside the sheet, but that doesn't make sense to me nor do I understand why that works.

Is this a bug, or am I missing something?

5
  • 1
    Use the sheet(item:) form instead of sheet(isPresented:) Commented Nov 23, 2022 at 23:54
  • @jnpdx using sheet(item:) will display the item when it's not nil but that's not the behavior I want. I want to display the sheet only when the user specifically presses a button. I also don't want to wait until the user presses the button to generate the content but generate the content in advance as it might take time. Commented Nov 24, 2022 at 7:01
  • All of the behavior you want can be done with the item: form Commented Nov 24, 2022 at 7:12
  • @jnpdx is right. This is a common problem. You don't have to use the content. Make a simple enum then. Check out this article: swiftjectivec.com/swiftui-sheet-present-item-vs-toggle/… Commented Nov 24, 2022 at 9:51
  • 1
    I still think that sheet(item:) is not the right use case here. And the article you pointed out (thanks for that) speaks about wanting to present something as soon as the item becomes non-nil - which isn't my case. I think @malhal answer is really the right solution here, making sure that the captured variable is updated Commented Nov 24, 2022 at 15:10

1 Answer 1

5

It's because the sheet's closure is created before it is shown and it has captured the old value of the link which was nil. To make it have the latest value, i.e. have a new closure created that uses the new value, then just add it to the capture list, e.g.

.sheet(isPresented: $showSheet) { [link] in

You can learn more about this problem in this answer to a different question. Also, someone who submitted a bug report on this was told by Apple to use the capture list.

By the way, .task is designed to remove the need for state objects for doing async work tied to view lifecycle. Also, you don't need MainActor.run.

Sign up to request clarification or add additional context in comments.

1 Comment

Brilliant. Thank you. Makes perfect sense now that you explained it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.