Deletable Table with TextField on SwiftUI
Ok, the reason is in documentation for used ForEach constructor (as you see range is constant, so ForEach grabs initial range and holds it):
/// Creates an instance that computes views on demand over a *constant*
/// range.
///
/// This instance only reads the initial value of `data` and so it does not
/// need to identify views across updates.
///
/// To compute views on demand over a dynamic range use
/// `ForEach(_:id:content:)`.
public init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)
So the solution would be to provide dynamic container. Below you can find a demo of possible approach.
Full module code
import SwiftUI
struct DummyView: View {
@State var animals: [String] = ["quot;, "quot;]
var body: some View {
VStack {
HStack {
EditButton()
Button(action: { self.animals.append("Animal \(self.animals.count + 1)") }, label: {Text("Add")})
}
List {
ForEach(animals, id: \.self) { item in
EditorView(container: self.$animals, index: self.animals.firstIndex(of: item)!, text: item)
}
.onDelete { indexSet in
self.animals.remove(atOffsets: indexSet) // Delete "quot; from animals
}
}
}
}
}
struct EditorView : View {
var container: Binding<[String]>
var index: Int
@State var text: String
var body: some View {
TextField("", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = self.text
})
}
}
backup
SwiftUI: cannot delete Row in List
The problem is that you are not using your items directly in the ForEach loop. Consider using structs for your data as objects and make them identifiable.
struct Player : Identifiable {
let id = UUID()
var stepperWerte: Int
var editText : String
}
struct ContentView : View {
@State var isEditing = false
@State var players = [Player(stepperWerte: 3, editText: "Player 1"), Player(stepperWerte: 5, editText: "Player 2"), Player(stepperWerte: 7, editText: "Player 3"), Player(stepperWerte: 9, editText: "Player 4")]
var startName = "new Player"
var startLeben = 5
var body: some View {
NavigationView {
List() {
ForEach(self.players) { player in
SecondView(player: player)
}
.onDelete(perform: spielerLoeschen)
}
.frame(width: nil, height: nil, alignment: .trailing)
.navigationBarTitle(Text("Nav_Title"))
.navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) },
trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
}
}
func spielerLoeschen(at offsets: IndexSet) {
players.remove(atOffsets: offsets)
}
func spielerHinzufuegen() {
players.insert(Player(stepperWerte: 4, editText: "Neuer Player"), at: 0)
}
}
struct SecondView : View {
var player : Player
@State var stepperWerte : Int
@State var name : String
init(player : Player)
{
self._stepperWerte = State(initialValue: player.stepperWerte)
self._name = State(initialValue: player.editText)
self.player = player
}
var body: some View
{
Stepper(value: self.$stepperWerte, in: -1...10, step: 1, label: {
TextField("", text: self.$name)
.layoutPriority(1)
.fixedSize(horizontal: true, vertical: false)
Text("\(player.stepperWerte)")
})
}
}
I created a struct Player
, and then an array of many Players. In the ForEach you can directly use players
as Player
confirms to Identifiable
protocol. This is way easier as you can access a player object in your ForEach loop and you do not have to access everything with indices. In the deleting function you just delete the object out of the array or add something new to it. Deleting now works fine.
I have removed some code from the list row, just to reproduce it easier, just if you are wondering.
Delete a Binding from a list in SwiftUI
This happens because you're enumerating by indices
and referencing binding by index inside ForEach
I suggest you switching to ForEachIndexed
: this wrapper will pass both index and a correct binding to your block:
struct ForEachIndexed<Data: MutableCollection&RandomAccessCollection, RowContent: View, ID: Hashable>: View, DynamicViewContent where Data.Index : Hashable
{
var data: [(Data.Index, Data.Element)] {
forEach.data
}
let forEach: ForEach<[(Data.Index, Data.Element)], ID, RowContent>
init(_ data: Binding<Data>,
@ViewBuilder rowContent: @escaping (Data.Index, Binding<Data.Element>) -> RowContent
) where Data.Element: Identifiable, Data.Element.ID == ID {
forEach = ForEach(
Array(zip(data.wrappedValue.indices, data.wrappedValue)),
id: \.1.id
) { i, _ in
rowContent(i, Binding(get: { data.wrappedValue[i] }, set: { data.wrappedValue[i] = $0 }))
}
}
init(_ data: Binding<Data>,
id: KeyPath<Data.Element, ID>,
@ViewBuilder rowContent: @escaping (Data.Index, Binding<Data.Element>) -> RowContent
) {
forEach = ForEach(
Array(zip(data.wrappedValue.indices, data.wrappedValue)),
id: (\.1 as KeyPath<(Data.Index, Data.Element), Data.Element>).appending(path: id)
) { i, _ in
rowContent(i, Binding(get: { data.wrappedValue[i] }, set: { data.wrappedValue[i] = $0 }))
}
}
var body: some View {
forEach
}
}
Usage:
ForEachIndexed($todoViewModel.todos) { index, todoBinding in
TextField("Test", text: todoBinding.title)
.contextMenu(ContextMenu(menuItems: {
VStack {
Button(action: {
self.todoViewModel.deleteAt(index)
}, label: {
Label("Delete", systemImage: "trash")
})
}
}))
}
SwiftUI: Deleting last row in ForEach
Here is fix
ForEach(Array(player.scores.enumerated()), id: \.element) { index, score in
HStack {
if self.isEditSelected {
Button(action: {
self.player.scores.remove(at: index)
}, label: {
Image("delete")
})
}
TextField("\(score)", value: Binding( // << use proxy binding !!
get: { self.player.scores[index] },
set: { self.player.scores[index] = $0 }),
formatter: NumberFormatter())
}
}
backup
Index Out Of Range When Using .onDelete -SwiftUI
I was able to get this to work by modifying my textfield to work the following way:
EditorView(container: self.$recipeStep, index: index, text: recipeStep[index])
The solution was found here, posted by Asperi: https://stackoverflow.com/a/58911168/12299030
I had to modify his solution a bit to fit my forEach, but overall it works perfectly!
Textfield with different borders in SwiftUI
I believe you can do it with a custom extension and view(code is below the image).
//main view
import SwiftUI
struct ContentView: View {
@State var text = ""
var body: some View {
VStack {
TextField("", text: $text)
.frame(width: 200, height: 40)
.border(width: 1, edges: [.bottom], color: .gray.opacity(0.4))
.border(width: 1, edges: [.top, .leading, .trailing], color: .gray.opacity(0.07))
}
}
}
//custom view
struct EdgeBorder: Shape {
var width: CGFloat
var edges: [Edge]
func path(in rect: CGRect) -> Path {
var path = Path()
for edge in edges {
var x: CGFloat {
switch edge {
case .top, .bottom, .leading: return rect.minX
case .trailing: return rect.maxX - width
}
}
var y: CGFloat {
switch edge {
case .top, .leading, .trailing: return rect.minY
case .bottom: return rect.maxY - width
}
}
var w: CGFloat {
switch edge {
case .top, .bottom: return rect.width
case .leading, .trailing: return self.width
}
}
var h: CGFloat {
switch edge {
case .top, .bottom: return self.width
case .leading, .trailing: return rect.height
}
}
path.addPath(Path(CGRect(x: x, y: y, width: w, height: h)))
}
return path
}
}
//custom extension
extension View {
func border(width: CGFloat, edges: [Edge], color: Color) -> some View {
overlay(EdgeBorder(width: width, edges: edges).foregroundColor(color))
}
}
for more info, please refer to this answer SwiftUI - Add Border to One Edge of an Image
Editbutton deletes items (with Bindings) from List in a funny way in SwiftUI
You can use enumerated
to access both the element
and its index
:
ForEach(Array(users.names.enumerated()), id: \.element.id) { index, element in
// use either `index` or `element` here
}
You can also use this extension to make sure you don't access non-existing bindings when deleting items.
Related Topics
How to Use Instance Method as Callback For Function Which Takes Only Func or Literal Closure
Wait Until Swift For Loop With Asynchronous Network Requests Finishes Executing
Trying to Understand Asynchronous Operation Subclass
Swift Constants: Struct or Enum
Flatten an Array of Arrays in Swift
How to Compare Enum With Associated Values by Ignoring Its Associated Value in Swift
Swiftui: How to Make Textfield Become First Responder
Waiting Until the Task Finishes
Passing an Array to a Function With Variable Number of Args in Swift
Swift - Resolving a Math Operation in a String
Load a Uiview from Nib in Swift
How to Create Usdz File Using Xcode Converter
How to Check If a Text Field Is Empty or Not in Swift