Deletable Table With Textfield on Swiftui

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).
Sample 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



Leave a reply



Submit