Reputation: 91
I'm trying to present a View in a sheet with a @Binding String variable that just shows/binds this variable in a TextField.
In my main ContentView I have an Array of Strings which I display with a ForEach looping over the indices of the Array, showing a Button each with the text of the looped-over-element.
The Buttons action is simple: set an @State "index"-variable to the pressed Buttons' Element-index and show the sheet.
Here is my ContentView:
struct ContentView: View {
@State var array = ["first", "second", "third"]
@State var showIndex = 0
@State var showSheet = false
var body: some View {
VStack {
ForEach (0 ..< array.count, id:\.self) { i in
Button("\(array[i])") {
showIndex = i
showSheet = true
}
}
// Text("\(showIndex)") // if I uncomment this line, it works!
}
.sheet(isPresented: $showSheet, content: {
SheetView(text: $array[showIndex])
})
.padding()
}
}
And here is the SheetView:
struct SheetView: View {
@Binding var text: String
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text)
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
The problem is, when I first open the app and press on the "second" Button, the sheet opens and displays "first" in the TextField. I can then dismiss the Sheet and press the "second" Button again with the same result.
If I then press the "third" or "first" Button everything works from then on. Pressing any Button results in the correct behaviour.
Interestingly, if I uncomment the line with the Text showing the showIndex-variable, it works from the first time on.
Is this a bug, or am I doing something wrong here?
Upvotes: 9
Views: 2633
Reputation: 443
I was able to fix this quite nicely by using a different sheet presentation modifier (that uses the nullability of the entity to determine if the sheet should be presented). So using
MyView()
.sheet(item: $item) { item in
MySheet(item: item)
}
instead of
MyView()
.sheet(isPresented: $isPresented) {
if let item {
MySheet(item: item)
}
}
In this simplified example, this way is undoubtedly better. In practice though, a view may need refactoring a little to work this way instead (e.g. grouping content into a structure).
To answer your specific question, this means replacing
@State var showSheet = false
// and
sheet(isPresented: $showSheet, content: {
SheetView(text: $array[showIndex])
})
with, say
@State private var selectedString: String?
// and
sheet(item: $selectedString) { selectedString in
SheetView(text: selectedString)
}
possibly using closures if you want to feedback completion.
I similarly was using a ForEach
with content that updates a State
via Binding
s in subviews.
Upvotes: 1
Reputation: 1331
This is how I would do it. You'll lose your form edits if you don't use @State variables.
This Code is Untested
struct SheetView: View {
@Binding var text: String
@State var draft: String
@Environment(\.presentationMode) var presentationMode
init(text: Binding<String>) {
self._text = text
self._draft = State(initialValue: text.wrappedValue)
}
var body: some View {
VStack {
TextField("text:", text: $draft)
Button("dismiss") {
text = draft
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
Upvotes: 0
Reputation: 83
Passing a binding to the index fix the issue like this
struct ContentView: View {
@State var array = ["First", "Second", "Third"]
@State var showIndex: Int = 0
@State var showSheet = false
var body: some View {
VStack {
ForEach (0 ..< array.count, id:\.self) { i in
Button(action:{
showIndex = i
showSheet.toggle()
})
{
Text("\(array[i])")
}.sheet(isPresented: $showSheet){
SheetView(text: $array, index: $showIndex)
}
}
}
.padding()
}
}
struct SheetView: View {
@Binding var text: [String]
@Binding var index: Int
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text[index])
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
In SwiftUI2 when calling isPresented if you don't pass bindings you're going to have some weird issues. This is a simple tweak if you want to keep it with the isPresented and make it work but i would advise you to use the item with a costum struct like the answer of swiftPunk
Upvotes: 0
Reputation: 1
You should use custom Binding, custom Struct for solving the issue, it is complex issue. See the Example:
struct ContentView: View {
@State private var array: [String] = ["first", "second", "third"]
@State private var customStruct: CustomStruct?
var body: some View {
VStack {
ForEach (array.indices, id:\.self) { index in
Button(action: { customStruct = CustomStruct(int: index) }, label: {
Text(array[index]).frame(width: 100)
})
}
}
.frame(width: 300, height: 300, alignment: .center)
.background(Color.gray.opacity(0.5))
.sheet(item: $customStruct, content: { item in SheetView(text: Binding.init(get: { () -> String in return array[item.int] },
set: { (newValue) in array[item.int] = newValue }) ) })
}
}
struct CustomStruct: Identifiable {
let id: UUID = UUID()
var int: Int
}
struct SheetView: View {
@Binding var text: String
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text)
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
Upvotes: 5
Reputation: 9755
I had this happen to me before. I believe it is a bug, in that until it is used in the UI, it doesn't seem to get set in the ForEach. I fixed it essentially in the same way you did, with a bit of subtlety. Use it in each Button
as part of the Label
but hide it like so:
Button(action: {
showIndex = i
showSheet = true
}, label: {
HStack {
Text("\(array[i])")
Text(showIndex.description)
.hidden()
}
})
This doesn't change your UI, but you use it so it gets properly updated. I can't seem to find where I had the issue in my app, and I have changed the UI to get away from this, but I can't remember how I did it. I will update this if I can find it. This is a bit of a kludge, but it works.
Upvotes: 1