pointfreeco/swift-navigation

sheet(unwrapping:) shows a blank view

Closed this issue · 1 comments

Description

When using .sheet(unwrapping:) to present a sheet when an optional value becomes non-nil, the sheet is always blank.

Given the following code from State-driven navigation:

struct FeatureView: View {
 @State var presentedValue: String?

 var body: some View {
   Button("Show sheet") {
     self.presentedValue = "Hello!"
   }
   .sheet(unwrapping: self.$presentedValue) { $value in
     TextField("Value", text: $value)
   }
 }
}

When I tap the "Show sheet" button, I'd expect to see a sheet with the TextField containing "Hello!". Instead, I see a blank sheet.

This seems to be broken on main as well as version 1.3.0, but it worked in version 0.6.1. I'm not sure which version introduced this issue.

Checklist

  • I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

I expected to see the TextField in a sheet with the text "Hello!".

Actual behavior

I saw a blank sheet.

Steps to reproduce

With the code above, this is what I see when I tap the button:

Simulator.Screen.Recording.-.iPhone.15.-.2024-05-10.at.14.21.46.mp4

Full project attached (though it's just the above code inside an otherwise empty project).

SheetUnwrappingIssue.zip

SwiftUI Navigation version information

1.3.0

Destination operating system

iOS 17.4

Xcode version information

15.3 (15E204a)

Swift Compiler version information

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0

Unfortunately this seems to be a SwiftUI bug with @State and derived bindings. If you change to use an observable object it works fine:

class FeatureModel: ObservableObject {
  @Published var presentedValue: String?
}

struct FeatureView: View {
  @ObservedObject var model = FeatureModel()

  var body: some View {
    Button("Show sheet") {
      self.model.presentedValue = "Hello!"
    }
    .sheet(unwrapping: self.$model.presentedValue) { $value in
      TextField("Value", text: $value)
    }
  }
}

It also works fine with the newer @Observable macro and @Bindable:

@Observable
class FeatureModel {
  var presentedValue: String?
}

struct FeatureView: View {
  @Bindable var model = FeatureModel()

  var body: some View {
    Button("Show sheet") {
      self.model.presentedValue = "Hello!"
    }
    .sheet(unwrapping: self.$model.presentedValue) { $value in
      TextField("Value", text: $value)
    }
  }
}

And it even works with @State if you create an ad hoc binding:

struct FeatureView: View {
  @State var presentedValue: String?

  var body: some View {
    Button("Show sheet") {
      self.presentedValue = "Hello!"
    }
    .sheet(
     unwrapping: Binding(
       get: { presentedValue },
       set: { presentedValue = $0 }
     )
   ) { $value in
      Text("Hi")
      TextField("Value", text: $value)
    }
  }
}

While we'd love to work around the bug in the library, I'm not sure if it's possible to detect if a binding is being derived from @State vs. something else. If anyone has an idea, though, we'd definitely be interested!

Note that ad hoc bindings can be buggy when it comes to SwiftUI animations and transactions, which is why this library prefers derived bindings. Sadly it seems that you should generally prefer to avoid @State in SwiftUI if you want to avoid @State-specific bugs.

Since this is a bug with SwiftUI and not the library, I'm going to convert to a discussion, but good find!