9

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?

1
  • I am using XCode Version 12.5.1 (12E507) and macOS Version 11.5.1 Commented Jul 31, 2021 at 19:21

5 Answers 5

5

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

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

Comments

1

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.

Comments

1

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

More Details

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.

Note

I similarly was using a ForEach with content that updates a State via Bindings in subviews.

2 Comments

This is a great solution - thanks!
With sheet(item:) the item is read only and the OP wanted a Binding for read/write.
0

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

Comments

0

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

Comments

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.