SwiftUI presenting sheet with Binding variable doesn't work when first presented

Viewed 910

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.

Preview

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?

4 Answers

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()
    }
}

enter image description here

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.

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

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()
    }
}
Related