Presenting sheet with Binding variable doesn't work when first presented
Asked Answered
L

5

8

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?

Lemuroid answered 31/7, 2021 at 19:18 Comment(1)
I am using XCode Version 12.5.1 (12E507) and macOS Version 11.5.1Lemuroid
D
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

Defoliate answered 31/7, 2021 at 22:16 Comment(0)
S
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.

Sc answered 31/7, 2021 at 19:49 Comment(0)
C
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

Calipash answered 31/7, 2021 at 22:33 Comment(0)
V
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()
    }
}
Vorlage answered 23/11, 2021 at 21:24 Comment(0)
D
0

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.

Describe answered 4/5, 2024 at 12:12 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.