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))
}
}
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))
}
}
Add animations to ForEach loop elements (SwiftUI)
It looks like this problem is still up to day (Xcode 11.4), because by just copy-pasting the observed effect is the same. So, there are a couple of problems here: first, it needs to setup combination of animation and transition correctly; and, second, the ForEach
container have to know which exactly item is removed, so items must be identified, instead of indices, which are anonymous.
As a result we have the following effect (transition/animation can be others):
struct TestAnimationInStack: View {
@State var ContentArray = ["A","B","C", "D", "E", "F", "G", "I", "J"]
var body: some View {
ScrollView{
VStack{
ForEach(Array(ContentArray.enumerated()), id: \.element){ (i, item) in // << 1) !
ZStack{
// Object
Text(item)
.frame(width:100,height:100)
.background(Color.gray)
.cornerRadius(20)
.padding()
//Delete button
Button(action: {
withAnimation { () -> () in // << 2) !!
self.ContentArray.remove(at: i)
}
}){
Text("✕")
.foregroundColor(.white)
.frame(width:40,height:40)
.background(Color.red)
.cornerRadius(100)
}.offset(x:40,y:-40)
}.transition(AnyTransition.scale) // << 3) !!!
}
}
}
}
}
How to animate the removal of a view created with a ForEach loop getting its data from an ObservableObject in SwiftUI
It is not clear which effect do you try to achieve, but on remove you should animate not view internals, but view itself, ie. in parent, because view remove there and as-a-whole.
Something like (just direction where to experiment):
var body: some View {
ZStack {
ForEach(tagModel.tags, id: \.self) { label in
TagView(label: label)
.transition(.move(edge: .leading)) // << here !! (maybe asymmetric needed)
}
.onReceive(timer) { _ in
self.tagModel.addNextTag()
if tagModel.tags.count > 3 {
self.tagModel.removeOldestTag()
}
}
}
.animation(Animation.easeInOut(duration: 1)) // << here !! (parent animates subview removing)
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 using ForEach and onTapGesture to update selected element causing crash
Your problem is likely due to the view diffing algorithm having a different outcome if you have the text value present, since that would be a difference in the root view. Since you have two pieces of state that represent the sheet, and you are force unwrapping one of them, you're leaving yourself open to danger.
You should get rid of showingSheet
, then use the item binding of sheet instead. Make Cards
conform to Identifiable
, then rewrite the sheet modifier as:
.sheet(item: $selectedCard) { SpecificCardView(card: $0) }
This will also reset your selected card to nil when the sheet is dismissed.
Related Topics
Error Using Associated Types and Generics
Swift Only -- Reading from Nsinputstream
How to Make a Uiview Focusable Using the Focus Engine on Apple Tv
Swift + Nsviewcontroller Background Color (MAC App)
Convincing Swift That a Function Will Never Return, Due to a Thrown Exception
Kvo with Shared Nsuserdefaults in Swift
Scenekit Object Between Two Points
Xcode 9 - Test Target X Encountered an Error (Unable to Connect to Test Manager)
Swift: Switch Between Nsviewcontroller Inside Container View/Nsview
Swift - How to Save Audio from Avaudioengine, or from Audioplayernode? If Yes, How
How to Implement a Spritekit Timer
How to Add Auto-Complete Comment in Xcode (Swift)
How to Use Swift Package Manager in Xcode 9 Playground