When I delete an element of an array using onDelete(), it removes the correct item in the data but removes the last item on the UI. I saw this answer but I am already using what it recommends. Any advice?
struct ContentView: View {
#State var items = ["One", "Two", "Three"]
var body: some View {
Form{
ForEach(items.indices, id:\.self){ itemIndex in
let item = self.items[itemIndex]
EditorView(container: self.$items, index: itemIndex, text: item)
}.onDelete(perform: { indexSet in
self.items.remove(atOffsets: indexSet)
})
}
}
}
Here is the EditorView struct:
struct EditorView : View {
var container: Binding<[String]>
var index: Int
#State var text: String
var body: some View {
TextField("Type response here", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = self.text
})
}
}
Your ForEach is looping over the indices of your array, so that is what SwiftUI is using to identify them. When you delete an item, you have one fewer index for the array, so SwiftUI interprets that as the last one has been deleted.
To do this correctly, you should be looping over a list of Identifiable items that have a unique id. Here I've created a struct called MyItem which holds the original String and a uniquely generated id. I use .map(MyItem.init) to convert the items into a [MyItem].
Also, your code needs the index in the loop, so loop over Array(items.enumerated()) which will give you an array of (offset, element) tuples. Then tell SwiftUI to use \.element.id as the id.
Note that EditorView now takes an Array of MyItem.
With these changes, SwiftUI will be able to identify the item you have deleted from the list and update the UI correctly.
struct MyItem: Identifiable {
var name: String
let id = UUID()
}
struct ContentView: View {
#State var items = ["One", "Two", "Three"].map(MyItem.init)
var body: some View {
Form{
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
EditorView(container: self.$items, index: index, text: item.name)
}.onDelete(perform: { indexSet in
self.items.remove(atOffsets: indexSet)
})
}
}
}
struct EditorView : View {
var container: Binding<[MyItem]>
var index: Int
#State var text: String
var body: some View {
TextField("Type response here", text: self.$text, onCommit: {
self.container.wrappedValue[self.index].name = self.text
})
}
}
Related
I'm very new to Swift and SwiftUI so apologies for the very basic question. I must be misunderstanding something about the SwiftUI lifecycle and it's interaction with #State.
I've have a list, and when you click on the row, it increments a counter. If I click on some row items to increment the counter, scroll down, and scroll back up again - the state is reset to 0 again. Can anyone point out where I'm going wrong? Many thanks.
struct TestView : View {
#State private var listItems:[String] = (0..<50).map { String($0) }
var body: some View {
List(listItems, id: \.self) { listItem in
TestViewRow(item: listItem)
}
}
}
struct TestViewRow: View {
var item: String
#State private var count = 0
var body: some View {
HStack {
Button(item, action: {
self.count += 1
})
Text(String(self.count))
Spacer()
}
}
}
Items in a List are potentially lazily-loaded, depending on the os (macOS vs iOS), length of the list, number of items on the screen, etc.
If a list item is loaded and then its state is changed, that state is not reassigned to the item if that item has been since unloaded/reloaded into the List.
Instead of storing #State on each List row, you could move the state to the parent view, which wouldn't be unloaded:
struct ContentView : View {
#State private var listItems:[(item:String,count:Int)] = (0..<50).map { (item:String($0),count:0) }
var body: some View {
List(Array(listItems.enumerated()), id: \.0) { (index,item) in
TestViewRow(item: item.item, count: $listItems[index].count)
}
}
}
struct TestViewRow: View {
var item: String
#Binding var count : Int
var body: some View {
HStack {
Button(item, action: {
count += 1
})
Text(String(count))
Spacer()
}
}
}
For some reason I don't understand, when I add/remove items from a #State var in MainView, the OutterViews are not being updated properly.
What I am trying to achieve is that the user can only "flag" (select) one item at a time. For instance, when I click on "item #1" it will be flagged. If I click on another item then "item #1" will not be flagged anymore but only the new item I just clicked.
Currently, my code shows all items as if they were flagged even when they are not anymore. The following code has the minimum structure and functionality I'm implementing for MainView, OutterView, and InnerView.
I've tried using State vars instead of the computed property in OutterView, but it doesn't work. Also, I tried using a var instead of the computed property in OutterViewand initialized it in init() but also doesn't work.
Hope you can help me to find what I am doing wrong.
Thanks!
struct MainView: View {
#State var flagged: [String] = []
var data: [String] = ["item #1", "item #2", "item #3", "item #4", "item #5"]
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(data, id:\.self) { text in
OutterView(text: text, flag: flagged.contains(text)) { (flag: Bool) in
if flag {
flagged = [text]
} else {
if let index = flagged.firstIndex(of: text) {
flagged.remove(at: index)
}
}
}
}
}
Text("Flagged: \(flagged.description)")
Button(action: {
flagged = []
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
#State private var flag: Bool
private let text: String
private var color: Color { flag ? Color.green : Color.gray }
private var update: (Bool)->Void
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
update(flag)
}
}
init(text: String, flag: Bool = false, update: #escaping (Bool)->Void) {
self.text = text
self.update = update
_flag = State(initialValue: flag)
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
Here's a simple version that does what you're looking for (explained below):
struct Item : Identifiable {
var id = UUID()
var flagged = false
var title : String
}
class StateManager : ObservableObject {
#Published var items = [Item(title: "Item #1"),Item(title: "Item #2"),Item(title: "Item #3"),Item(title: "Item #4"),Item(title: "Item #5")]
func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.items[index].flagged
} set: { (newValue) in
self.items = self.items.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.flagged = newValue
} else {
//not the same index
if newValue {
itemCopy.flagged = false
}
}
return itemCopy
}
}
}
func reset() {
items = items.map { item in
var itemCopy = item
itemCopy.flagged = false
return itemCopy
}
}
}
struct MainView: View {
#ObservedObject var stateManager = StateManager()
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(Array(stateManager.items.enumerated()), id:\.1.id) { (index,item) in
OutterView(text: item.title, flag: stateManager.singularBinding(forIndex: index))
}
}
Text("Flagged: \(stateManager.items.filter({ $0.flagged }).map({$0.title}).description)")
Button(action: {
stateManager.reset()
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
var text: String
#Binding var flag: Bool
private var color: Color { flag ? Color.green : Color.gray }
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
}
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
What's happening:
There's a Item that has an ID for each item, the flagged state of that item, and the title
StateManager keeps an array of those items. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the item array. Any time a checkbox is set, it unchecks all of the other boxes.
The ForEach now gets an enumeration of the items. This could be done without enumeration, but it was easy to write the custom binding by index like this. You could also filter by ID instead of index. Note that because of the enumeration, it's using .1.id for the id parameter -- .1 is the item while .0 is the index.
Inside the ForEach, the custom binding from before is created and passed to the subview
In the subview, instead of using #State, #Binding is used (this is what the custom Binding is passed to)
Using this strategy of an ObservableObject that contains all of your state and passes it on via #Published properties and #Bindings makes organizing your data a lot easier. It also avoids having to pass closures back and forth like you were doing initially with your update function. This ends up being a pretty idiomatic way of doing things in SwiftUI.
I refer to two questions that I already asked and have been answered very well by Asperi: SwiftUI ForEach with .indices() does not update after onDelete,
SwiftUI onDelete List with Toggle
Now I tried to modify the closure in ForEach with a NavigationLink and suddenly the App crashes again with
Thread 1: Fatal error: Index out of range
when I try to swipe-delete.
Code:
class Model: ObservableObject {
#Published var name: String
#Published var items: [Item]
init(name: String, items: [Item]) {
self.name = name
self.items = items
}
}
struct Item: Identifiable {
var id = UUID()
var isOn: Bool
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
List {
ForEach(model.items) {item in
NavigationLink(destination: DetailView(item: self.makeBinding(id: item.id))) {
Toggle(isOn: self.makeBinding(id: item.id).isOn)
{Text("Toggle-Text")}
}
}.onDelete(perform: delete)
}
}
}
func delete(at offsets: IndexSet) {
self.model.items.remove(atOffsets: offsets)
}
func makeBinding(id: UUID) -> Binding<Item> {
guard let index = self.model.items.firstIndex(where: {$0.id == id}) else {
fatalError("This person does not exist")
}
return Binding(get: {self.model.items[index]}, set: {self.model.items[index] = $0})
}
}
struct DetailView: View {
#Binding var item: Item
var body: some View {
Toggle(isOn: $item.isOn) {
Text("Toggle-Text")
}
}
}
It works without NavigationLink OR without the Toggle. So it seems for me that I only can use the makeBinding-Function once in this closure.
Thanks for help
Your code was crashing for me with and even without Navigation Link. Sometimes only if I deleted the last object in the Array. It looks like it was still trying to access an index out of the array. The difference to your example you linked above, is that they didn't used EnvironmentObject to access the array. The stored the array directly in the #State.
I came up with a little different approach, by declaring Item as ObservedObject and then simply pass it to the subview where you can use their values as Binding, without any function.
I changed Item to..
class Item: ObservableObject {
var id = UUID()
var isOn: Bool
init(id: UUID, isOn: Bool)
{
self.id = id
self.isOn = isOn
}
}
Change the ContentView to this..
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
List {
ForEach(model.items, id:\.id) {item in
NavigationLink(destination: DetailView(item: item)) {
Toggler(item: item)
}
}.onDelete(perform: delete)
}
}
}
I outsourced the Toggle to a different view, where we pass the ObservedObject to, same for the DetailView.
struct Toggler: View {
#ObservedObject var item : Item
var body : some View
{
Toggle(isOn: $item.isOn)
{Text("Toggle-Text")}
}
}
struct DetailView: View {
#ObservedObject var item: Item
var body: some View {
Toggle(isOn: $item.isOn) {
Text("Toggle-Text")
}
}
}
They both take an Item as ObservedObject and use it as Binding for the Toggle.
I am looking for some guidance with SwiftUI please.
I have a view showing a simple list with each row displaying a "name" string. You can add items to the array/list by clicking on the trailing navigation bar button. This works fine. I would now like to use NavigationLink to present a new "DetailView" in which I can edit the row's "name" string. I'm struggling with how to use a binding in the detailview to update the name.
I've found plenty of tutorials online on how to present data in the new view, but nothing on how to edit the data.
Thanks in advance.
ContentView:
struct ListItem: Identifiable {
let id = UUID()
let name: String
}
class MyListClass: ObservableObject {
#Published var items = [ListItem]()
}
struct ContentView: View {
#ObservedObject var myList = MyListClass()
var body: some View {
NavigationView {
List {
ForEach(myList.items) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item.name)
}
}
}
.navigationBarItems(trailing:
Button(action: {
let item = ListItem(name: "Test")
self.myList.items.append(item)
}) {
Image(systemName: "plus")
}
)
}
}
}
DetailView
struct DetailView: View {
var item: ListItem
var body: some View {
TextField("", text: item.name)
}
}
The main idea that you pass in DetailsView not item, which is copied, because it is a value, but binding to the corresponding item in your view model.
Here is a demo with your code snapshot modified to fulfil the requested behavior:
struct ListItem: Identifiable, Equatable {
var id = UUID()
var name: String
}
class MyListClass: ObservableObject {
#Published var items = [ListItem]()
}
struct ContentView: View {
#ObservedObject var myList = MyListClass()
var body: some View {
NavigationView {
List {
ForEach(myList.items) { item in
// Pass binding to item into DetailsView
NavigationLink(destination: DetailView(item: self.$myList.items[self.myList.items.firstIndex(of: item)!])) {
Text(item.name)
}
}
}
.navigationBarItems(trailing:
Button(action: {
let item = ListItem(name: "Test")
self.myList.items.append(item)
}) {
Image(systemName: "plus")
}
)
}
}
}
struct DetailView: View {
#Binding var item: ListItem
var body: some View {
TextField("", text: self.$item.name)
}
}
I'm starting with SwiftUI and I'm running into a roadblock with array items of an ObservableObject not saving to the main object.
Main object:
class Batch: Codable, Identifiable, ObservableObject {
let id: String
var items = [Item]()
}
Item object:
class Item: Codable, Identifiable, ObservableObject {
let id: String
var description: String
}
I have a BatchView which I pass a batch into:
struct BatchView: View {
#ObservedObject var batch: Batch
var body: some View {
List {
ForEach(batch.items) { item in
ItemView(item: item)
}
}
.navigationBarTitle(batch.items.reduce("", { $0 + $1.description }))
}
}
In the ItemView I change the description:
struct ItemView: View {
#ObservedObject var item: Item
#State private var descr = ""
var body: some View {
VStack(alignment: .leading) {
Text("MANUFACTURED")
TextField("", text: $descr) {
self.updateDescr(descr: self.descr)
}
}
}
private func updateDescr(descr: String) {
item.description = descr
}
}
But when I update the description for a batch item, the title of BatchView doesn't change, so the changes to the Item isn't coming back to the root Batch.
How do I make the above work?
This answer helped me. I had to explicitly add #Published in front of the variable I was changing.