0

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?

Ron Srebro
  • 6,663
  • 3
  • 27
  • 40
  • 1
    Use the `sheet(item:)` form instead of `sheet(isPresented:)` – jnpdx Nov 23 '22 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. – Ron Srebro Nov 24 '22 at 07:01
  • All of the behavior you want can be done with the item: form – jnpdx Nov 24 '22 at 07: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: https://www.swiftjectivec.com/swiftui-sheet-present-item-vs-toggle/?utm_campaign=iOS%2BDev%2BWeekly&utm_medium=email&utm_source=iOS%2BDev%2BWeekly%2BIssue%2B582 – bjorn.lau Nov 24 '22 at 09: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 – Ron Srebro Nov 24 '22 at 15:10

1 Answers1

1

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.

malhal
  • 26,330
  • 7
  • 115
  • 133