Swiftui - Foreach Deletion Transition Always Applied to Last Item Only

SwiftUI - ForEach deletion transition always applied to last item only

The reason you're seeing this behavior is because you use an index as an id for ForEach. So, when an element is removed from the cards array, the only difference that ForEach sees is that the last index is gone.

You need to make sure that the id uniquely identifies each element of ForEach.

If you must use indices and have each element identified, you can either use the enumerated method or zip the array and its indices together. I like the latter:

ForEach(Array(zip(cards.indices, cards)), id: \.1) { (index, card) in 
//...
}

The above uses the object itself as the ID, which requires conformance to Hashable. If you don't want that, you can use the id property directly:

ForEach(Array(zip(cards.indices, cards)), id: \.1.id) { (index, card) in
//...
}

For completeness, here's the enumerated version (technically, it's not an index, but rather an offset, but for 0-based arrays it's the same):

ForEach(Array(cards.enumerated()), id: \.1) { (index, card) in 
//...
}

SwiftUI custom list with ForEach delete animation not working

You need to make each row uniquely identified, so animator know what is added and what is removed, so animate each change properly.

Here is possible approach. Tested with Xcode 12 / iOS 14

Sample Image

struct TimeItem: Identifiable, Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}

let id = UUID() // << identify item
let minutes: Int
let seconds: Int = 0
}

struct ContentView: View {
@State var items = [TimeItem]()
@State var selectedElement: TimeItem?

var body: some View {
ScrollView(){
VStack{
ForEach(items){ elem in // << work by item

ZStack{

EntryBackground()


Text("\(elem.minutes)")
.transition(AnyTransition.scale)

HStack{
Button(action: {
self.items.removeAll { $0.id == elem.id }
})
{
Image(systemName: "minus.circle.fill")
.foregroundColor(Color.red)
.font(.system(size: 22))
.padding(.leading, 10)
}
Spacer()
}

}
.padding(.horizontal)
.padding(.top)
.contentShape(Rectangle())
.onTapGesture {
withAnimation(.spring()){
self.selectedElement = elem
}
}
}
}
Spacer()

Button(action: {
self.items.append(TimeItem(minutes: self.items.count))
})
{
ZStack{
EntryBackground()

Text("Add")

HStack{
Image(systemName: "plus.circle.fill")
.foregroundColor(Color.green)
.font(.system(size: 22))
.padding(.leading, 10)

Spacer()
}
}.padding()
}
}.animation(.spring(), value: items) // << animate changes
}
}

struct EntryBackground: View {
var body: some View {
Rectangle()
.cornerRadius(12)
.frame(height: 40)
.foregroundColor(Color.gray.opacity(0.15))
}
}

Insert, update and delete animations with ForEach in SwiftUI

Luckily this is actually really easy to do. Simply remove .animation(.spring()) on your Row, and wrap any changes in withAnimation(.spring()) { ... }.

So the add button will look like this:

private var AddButton: some View {
Button(action: {
withAnimation(.spring()) {
self.items.insert(Item(name: "Jeff"), at: 0)
}
}) {
Text("Add")
}
}

and your Row will look like this:

struct Row: View {

@State var name: String

var body: some View {
HStack {
Text(name)
Spacer()
}
.padding()
.transition(.move(edge: .leading))
}
}

Removing items from a child view generated by For Each loop causes Fatal Error

This answer is similar to
Accessing and manipulating array item in an EnvironmentObject


Loop over superReminders.reminderlist since SuperReminder: Identifiable, Codable, Equatable.

ForEach(superReminders.reminderlist) { superReminder in
NavigationLink(destination: DetailedRemView(superReminders: superReminders,
superReminder: superReminder)) {
-----
}
}

In DetailedRemView, do the following:

struct DetailedRemView: View {

@ObservedObject var superReminders : SuperReminders

var superReminder : SuperReminder

// find index of current superReminder
var indexOfReminder: Int? {
superReminders.reminderlist.firstIndex {$0 == superReminder}
}

var body: some View {
// Unwrap indexOfReminder
if let index = indexOfReminder {
VStack {
------
}
}
}


----

}

Use superReminders.reminderlist[index] in DetailRemView whereever you need to update superReminder.

superReminders.reminderlist[index].pastreminder = false

Toggle selection in a list - SwiftUI

Your @State private var toggle: Bool = false doesn't make sense. You have many courses, not a single course. Each course should have it's own toggle on/off, which is what you started to do with:

struct Course: Codable, Identifiable {
var isToggled = false /// here!

...
}

To use this, you can reference each course's isToggled inside the ForEach, like this:

ForEach(courses) { course in

Section(header: Text(course.title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
ForEach(course.courseName, id: \.name) { item in

/// here!
Toggle(isOn: course.isToggled, label: {
Text(item.name)
})

}
}
}

However, this won't work. course.isToggled is a Bool, not a Binding<Bool>, which the Toggle expects.

Where can you get Binding<Bool>? From the @State var courses: [Course], of course! sorry for pun



The Binding<> part comes from the @State declaration.

Properties that are marked with @State, like your @State var courses: [Course], include a projectedValue that has the Binding<> type.

You can access the projectedValue by adding a $ to the property. So, if you write $courses, that will have type Binding<[Course]>.

Xcode autocompletion for $courses

But, your toggle expects Binding<Bool>, not Binding<[Course]>.

Toggle(isOn: Binding, label: { Text("A cool toggle") })

This is where the Bool part comes in.

You will need to replace the Binding's value, [Course], with a Bool. Well, we had a Bool before, right?

struct Course: Codable, Identifiable {
var isToggled = false /// this is a Bool!

Each course has a isToggled, which is a Bool. From earlier on in this answer, we got this inside the ForEach:

ForEach(courses) { course in

...

/// getting the Bool, which unfortunately doesn't work (yet)
Toggle(isOn: course.isToggled, label: {

... We need to somehow combine the Binding<> with the Bool. This means that we must

  • reference $courses (to get the Binding<>)
  • get each courses' isToggled

And... tada!

$courses[index].isToggled /// has type Binding<Bool>

To get index, we'll need to loop over courses.indices instead of directly looping over courses.

ForEach(courses.indices) { index in

...

/// this works!
Toggle(isOn: $courses[index].isToggled, label: {

Then, just replace every occurrence of course in your old code's ForEach with courses[index]. Here's the full working example:

ForEach(courses.indices) { index in
Section(header: Text(courses[index].title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
ForEach(courses[index].courseName, id: \.name) { item in

/// $courses[index].isToggled is a Binding<Bool>
Toggle(isOn: $courses[index].isToggled, label: {
Text(item.name)
})
}
}
}

As a convenience so you don't have to do courses[index] every time you want the current course, you can use Array(zip as shown in this answer to loop over a (Int, Course). This also assigns a unique id for every Section inside the loop, so any transitions you add will work out smoothly.

ForEach(Array(zip(courses.indices, courses)), id: \.1.id) { (index, course) in

Section(header: Text(course.title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
ForEach(course.courseName, id: \.name) { item in

Toggle(isOn: $courses[index].isToggled, label: {
Text(item.name)
})
}
}
}

Well (Int, Course) is actually (Range<Array<Course>.Index>.Element, Course) but that's pretty much the same thing.

Final result:

Toggles inside each row of the ForEach working

Edit for isToggled inside Content, not Course:

ForEach(Array(zip(courses.indices, courses)), id: \.1.id) { (index, course) in
Section(header: Text(course.title).font(.system(size: 15, weight: .medium, design: .rounded)).foregroundColor(.blue)) {
ForEach(Array(zip(course.courseName.indices, course.courseName)), id: \.1.id) { (itemIndex, item) in

/// here!
Toggle(isOn: $courses[index].courseName[itemIndex].isToggled, label: {
Text(item.name)
})
}
}
}

How to update an element of an array in an Observable Object

The problem is here:

struct ActivityDetailView: View {
@State var activity: Activity
...

This needs to be a @Binding in order for changes to be reflected back in the parent view. There's also no need to pass in the entire viewModel in - once you have the @Binding, you can get rid of it.

// detail view
struct ActivityDetailView: View {
@Binding var activity: Activity /// here!

var body: some View {
VStack {
Text("Number of times completed: \(activity.completions)")
Button("Increment completion count"){
activity.completions += 1
}
Text("\(activity.completeDescription)")
}
}
}

But how do you get the Binding? If you're using iOS 15, you can directly loop over $viewModel.activities:

/// here!
ForEach($viewModel.activities, id: \.id) { $activity in
NavigationLink(destination: ActivityDetailView(activity: $activity)) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}

And for iOS 14 or below, you'll need to loop over indices instead. But it works.

/// from https://stackoverflow.com/a/66944424/14351818
ForEach(Array(zip(viewModel.activities.indices, viewModel.activities)), id: \.1.id) { (index, activity) in
NavigationLink(destination: ActivityDetailView(activity: $viewModel.activities[index])) {
HStack {
VStack {
Text(activity.name)
Text(activity.miniDescription)
}
Text("\(activity.completions)")
}
}
}

Accessing and manipulating array item in an EnvironmentObject

This approach is similar to Apple's in this tutorials.

https://developer.apple.com/tutorials/swiftui/handling-user-input


Confirm to Identifiable and Equatable.


struct Activity: Codable, Identifiable, Equatable {
var id: UUID
var name: String
// var Records: [Record]

init(Name:String) {
self.id = UUID()
self.name = Name
// self.Records = [Record]()
}
}

Iterate over activity.activities and pass your view-model and activity to ActivityListItemView

   ForEach(viewModel.activities) { activity in
HStack {
ActivityListItemView(viewModel: viewModel, activity: activity)
}
}

In ActivityListItemView, find index of its activity

private var activityIndex: Int? {
viewModel.activities.firstIndex(of: activity)
}

Unwrap activityIndex and pass $viewModel.activities[index] to ActivityDetail

var body: some View {
if let index = activityIndex {
NavigationLink(destination: ActivityDetail(activity: $viewModel.activities[index])) {
...
}
...
}
}

Use @Binding wrapper in ActivityDetail.


struct ActivityDetail: View {
@Binding var activity: Activity

var body: some View {
...
}
}

A complete working exammple.

class ActivityViewModel: ObservableObject {
@Published var activities = [Activity]()

init() {
self.activities = [Activity(Name: "A"), Activity(Name: "B"), Activity(Name: "C"), Activity(Name: "D"), Activity(Name: "E")]
}
}

struct Activity: Codable, Identifiable, Equatable {
var id: UUID
var name: String
// var Records: [Record]

init(Name:String) {
self.id = UUID()
self.name = Name
// self.Records = [Record]()
}
}


struct ActivityView: View {
@ObservedObject var viewModel = ActivityViewModel()
var body: some View {
Button(action: {
self.viewModel.activities.append(Activity(Name: "\(Date())"))
}, label: {
Text("Button")
})
ForEach(viewModel.activities) { activity in
HStack {
ActivityListItemView(viewModel: viewModel, activity: activity)
}
}

}
}


struct ActivityListItemView: View {

@ObservedObject var viewModel: ActivityViewModel

let activity: Activity

private var activityIndex: Int? {
viewModel.activities.firstIndex(of: activity)
}

var body: some View {
if let index = activityIndex {
NavigationLink(destination: ActivityDetail(activity: $viewModel.activities[index])) {
HStack {
VStack {
HStack {
Text(activity.name)
// Text("\(activity.Records.count) records")
}
}
Text(">")
}
}
.buttonStyle(PlainButtonStyle())
}
}
}


struct ActivityDetail: View {
@Binding var activity: Activity

var body: some View {
Text("\(activity.name)")
}
}



Related Topics



Leave a reply



Submit