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
Bundle.Main.Path(Forresource... Always Returns Nil When Looking for Xml File
Load Viewcontroller Swift - Black Screen
Swift Cannot Convert The Expression's Type 'Void' to Type 'string!'
Different UIfont Sizes for Different iOS Devices in Swift
Is The for Loop Condition Evaluated Each Loop in Swift
Type 'Int' Does Not Conform to Protocol 'Booleantype'
iOS App Extension - Action - Custom Data
How to Access to a Static Cell
Views Are Merged Instead of Showing Them Separately
Windownibname Error in Swift/Cocoa
What Are The Benefits of an Immutable Struct Over a Mutable One
How to Detect Hash Changes in Wkwebview
Swift Wkwebview: Can't Find Variable Error When Calling Method
Negative Arrayslice: Index Is Out of Range
Edit Templates Authentication Firebase
Can't Upload .Ipa from Xcode 8, "The Info.Plist Indicates a iOS App, But Submitting a Pkg or Mpkg."