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
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]>
.
But, your toggle expects Binding<Bool>
, not Binding<[Course]>
.
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 theBinding<>
) - 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:
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
At Runtime, How Does Swift Know Which Implementation to Use
What Are the Precedence Levels of the Swift Operators
Image Upload in Vapor 3 Using Postgresql
Why Does Swift Return an Unexpected Pointer When Converting an Optional String into an Unsafepointer
Swiftui: Navigation Bar Title in Reusable Cross-Platform (iOS & MACos) View
Break on Any Occurrence of "Fatal Error: Unexpectedly Found Nil While Unwrapping an Optional Value"
Swift String VS. String! VS. String
Where Is the .Camera Anchorentity Located
Swift 4 Attributedstring Get Typing Attributes
Swiftui @Fetchrequest Core Data Changes to Relationships Don't Refresh
Scenekit Shadow on a Transparent Scnfloor()
Reason for Assigning Optional to New Variable in Conditional Statement in Swift
Getting Results from Arbitrary SQL Statements with Correct Binding in SQLite.Swift
Self.Image.Frame.Width = 20 Give Get Only Property Error
Why Can't I Pass an Implicitly Unwrapped Optional as an Unsafemutablepointer