The app has the following setup:
My main view creates a tag cloud using a SwiftUI ForEach
loop.
The ForEach
gets its data from the @Published
array of an ObservableObject
called TagModel
. Using a Timer
, every three seconds the ObservableObject
adds a new tag to the array.
By adding a tag the ForEach
gets triggered again and creates another TagView
. Once more than three tags have been added to the array, the ObservableObject
removes the first (oldest) tag from the array and the ForEach removes that particular TagView
.
With the following problem:
The creation of the TagViews works perfect. It also animates the way it's supposed to with the animations and .onAppear modifiers of the TagView
. However when the oldest tag is removed it does not animate its removal. The code in .onDisappear
executes but the TagView
is removed immediately.
I tried the following to solve the issue:
I tried to have the whole appearing and disappearing animations of TagView
run inside the .onAppear
by using animations that repeat and then autoreverse.
It sort of works but this way there are two issues.
First, if the animation is timed too short, once the animation finishes and the TagView
is removed, will show up for a short moment without any modifiers applied, then it will be removed.
Second, if I set the animation duration longer the TagView
will be removed before the animation has finished.
In order for this to work I'd need to time the removal and the duration of the animation very precisely, which would make the TagView
very dependent on the Timer
and this doesn't seem to be a good solution.
Another solution I tried was finding something similar to self.presentationMode.wrappedValue.dismiss()
using the @Environment(\.presentationMode)
variable and somehow have the TagView
remove itself after the .onAppear
animation has finished. But this only works if the view has been created in an navigation stack and I couldn't find any other way to have a view destroy itself. Also I assume that would again cause issue as soon as TagModel
updates its array.
I read several other S.O. solution that pointed towards the enumeration of the data in the ForEach loop. But I'm creating each TagView as its own object, I'd assume this should not be the issue and I'm not sure how I'd have to implement this if this is part of the issue.
Here is the simplified code of my app that can be run in an iOS single view SwiftUI project.
import SwiftUI
struct ContentView: View {
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
@ObservedObject var tagModel = TagModel()
var body: some View {
ZStack {
ForEach(tagModel.tags, id: \.self) { label in
TagView(label: label)
}
.onReceive(timer) { _ in
self.tagModel.addNextTag()
if tagModel.tags.count > 3 {
self.tagModel.removeOldestTag()
}
}
}
}
}
class TagModel: ObservableObject {
@Published var tags = [String]()
func addNextTag() {
tags.append(String( Date().timeIntervalSince1970 ))
}
func removeOldestTag() {
tags.remove(at: 0)
}
}
struct TagView: View {
@State private var show: Bool = false
@State private var position: CGPoint = CGPoint(x: Int.random(in: 50..<250), y: Int.random(in: 10..<25))
@State private var offsetY: CGFloat = .zero
let label: String
var body: some View {
let text = Text(label)
.opacity(show ? 1.0 : 0.0)
.scaleEffect(show ? 1.0 : 0.0)
.animation(Animation.easeInOut(duration: 6))
.position(position)
.offset(y: offsetY)
.animation(Animation.easeInOut(duration: 6))
.onAppear() {
show = true
offsetY = 100
}
.onDisappear() {
show = false
offsetY = 0
}
return text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}