How to Animate The Removal of a View Created with a Foreach Loop Getting Its Data from an Observableobject in Swiftui

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 & MVVM: How to animate list elements change when view model changes the data source (`@Publish items`) itself

Add animation to the container which hold your view items, like below

var body: some View {
VStack { // << container
ForEach(items) { item in
Text(item)
}
}
.animation(.default) // << animates changes in items
}

See next posts for complete examples: https://stackoverflow.com/a/60893462/12299030, https://stackoverflow.com/a/65776506/12299030, https://stackoverflow.com/a/63364795/12299030.

SwiftUI - How to animate components corresponding to array elements?

a version with automatic scroll to the last circle:

struct myItem: Identifiable, Equatable {
let id = UUID()
var size: CGFloat
}

struct ContentView: View {

@State private var myArr: [myItem] = [
myItem(size: 10),
myItem(size: 40),
myItem(size: 30)
]

var body: some View {

ScrollViewReader { scrollProxy in
VStack(alignment: .leading) {
Spacer()

ScrollView(.horizontal) {
HStack {
ForEach(myArr) { item in
Circle()
.id(item.id)
.frame(width: item.size, height: item.size)
.transition(.scale)
}
}
}
.animation(.easeInOut(duration: 1), value: myArr)

Spacer()

Button("Add One") {
let new = myItem(size: CGFloat.random(in: 10...100))
myArr.append(new)
}

.onChange(of: myArr) { _ in
withAnimation {
scrollProxy.scrollTo(myArr.last!.id, anchor: .trailing)
}
}

.frame(maxWidth: .infinity, alignment: .center)
}
.padding()
}
}
}

SwiftUI list view not reflecting removal of correct view model object?

This can be solved without static members and Combine. I would pass a closure to each PostCell which is the action that gets performed when the user deletes/reports the post:

struct NewsFeed: View {
@StateObject var newsfeedVM = NewsFeedViewModel()

var body: some View {
NavigationView {
List(newsfeedVM.posts, id: \.id) { post in
PostCell(post: post, removePost: {vm.removePost(post.id)} ) // use the ID not the index
}
}
}
}
class NewsFeedViewModel: ObservableObject {
@Published var posts: [Post] = []

// var subscriptions = Set<AnyCancellable>()

// none of the static stuff: static let removePostFromArray = PassthroughSubject<String, Never>()

func removePost(id: Int) {
posts.removeAll(where: {$0.id == id})
}
}
struct PostCell: View {
@StateObject var postVM = PostViewModel()
@State var post: Post
@State var showActionSheet: Bool = false
var removePost: func () -> ()

var body: some View {
VStack(spacing: 5) {
PostMedia(post: $post)
}
.actionSheet(isPresented: $showActionSheet) { "REPORT POST" } // call report() here
}

// use a closure instead
func report() {
// not this: NewsFeedViewModel.removePostFromArray.send(report.postId)
removePost()
}
}

edit: if you want to have access to index, you can pass an array of tuples (index, item) to List (only tested this with ForEach. should work though):

List(Array(newsfeedVM.posts.enumerated()), id: \.index) { index, item in
// ... now you have both index and item. you use item to draw the views
}

How to add a modifier to any specific buttons inside a ForEach loop for an array of buttons in SwiftUI?

import SwiftUI

struct AnimatedListView: View {
var body: some View {
VStack{
ForEach(0..<5) { num in
//The content of the ForEach goes into its own View
AnimatedButtonView(num: num)
}
}
}
}
struct AnimatedButtonView: View {
//This creates an @State for each element of the ForEach
@State private var rotationDegree = 0.0
//Pass the loops data as a parameter
let num: Int
var body: some View {
Button {
withAnimation {
rotationDegree += 360
}
} label: {
Image(systemName: "person")
Text(num.description)
}
.rotation3DEffect((.degrees(rotationDegree)), axis: (x: 0, y: 1, z: 0))
}
}
struct AnimatedListView_Previews: PreviewProvider {
static var previews: some View {
AnimatedListView()
}
}

Combine + SwiftUI Form + RunLoop causes table view to render unpredictably

@Asperi's suggestion got me on the right track thinking about how many withAnimation { } events would get called. In my original question, filteredItems and chosenItems would be changed in different iterations of the RunLoop when receive(on:) or debounce was used, which seemed to be the root cause of the unpredictable layout behavior.

By changing the debounce time to a longer value, this would prevent the issue, because one animation would be done after the other was finished, but was a problematic solution because it relied on the animation times (and potentially magic numbers if explicit animation times weren't sent).

I've engineered a somewhat tacky solution that uses a PassThroughSubject for chosenItems instead of assigning to the @Published property directly. By doing this, I can move all assignment of the @Published values into the sink, resulting in just one animation block happening.

I'm not thrilled with the solution, as it feels like an unnecessary hack, but it does seem to solve the issue:


class Completer : ObservableObject {
@Published var items : [Item] = [] {
didSet {
setupPipeline()
}
}
@Published private(set) var filteredItems : [Item] = []
@Published private(set) var chosenItems: Set<Item> = []
@Published var searchTerm = ""

private var chosenPassthrough : PassthroughSubject<Set<Item>,Never> = .init()
private var filterCancellable : AnyCancellable?

private func setupPipeline() {
filterCancellable =
Publishers.CombineLatest($searchTerm,chosenPassthrough)
.debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
.map { (term,chosen) -> (filtered: [Item],chosen: Set<Item>) in
if term.isEmpty {
return (filtered: self.items, chosen: chosen)
} else {
return (filtered: self.items.filter { $0.name.localizedStandardContains(term) }, chosen: chosen)
}
}
.map { (filtered,chosen) in
(filtered: filtered.filter { !chosen.contains($0) }, chosen: chosen)
}
.sink { [weak self] (filtered, chosen) in
withAnimation {
self?.filteredItems = filtered
self?.chosenItems = chosen
}
}
chosenPassthrough.send([])
}

func toggleItemChosen(item: Item) {
if chosenItems.contains(item) {
var copy = chosenItems
copy.remove(item)
chosenPassthrough.send(copy)
} else {
var copy = chosenItems
copy.insert(item)
chosenPassthrough.send(copy)
}
searchTerm = ""
}

func clearChosen() {
chosenPassthrough.send([])
}
}

struct ContentView: View {
@StateObject var completer = Completer()

var body: some View {
Form {
Section {
TextField("Term", text: $completer.searchTerm)
}
Section {
ForEach(completer.filteredItems) { item in
Button(action: {
completer.toggleItemChosen(item: item)
}) {
Text(item.name)
}.foregroundColor(completer.chosenItems.contains(item) ? .red : .primary)
}
}
if completer.chosenItems.count != 0 {
Section(header: HStack {
Text("Chosen items")
Spacer()
Button(action: {
completer.clearChosen()
}) {
Text("Clear")
}
}) {
ForEach(Array(completer.chosenItems)) { item in
Button(action: {
completer.toggleItemChosen(item: item)
}) {
Text(item.name)
}
}
}
}
}.onAppear {
completer.items = ["Chris", "Greg", "Ross", "Damian", "George", "Darrell", "Michael"]
.map { Item(name: $0) }
}
}
}

struct Item : Identifiable, Hashable, Equatable {
var id = UUID()
var name : String
}


Related Topics



Leave a reply



Submit