Hello everyone,
During the week I decided to make a classic loading view where you have the text “Loading” followed by three dots appearing after the text in a looping sequence. As I was trying to solve this, I encountered some difference in behavior when trying to use it with the animation modifier and when I used withAnimation.
As it usually goes, withAnimation comes out as the winner when you try to do certain animations in a sequence that repeats.
I haven’t had the time yet to investigate how to implement videos in a substack post, but I guess that a gif format would work. Maybe. So what you have to do is just copy and paste the code in order to test the three different code examples out and see how they behave. Or you can watch my twitter link at the end of this post as I will show a video there.
Let’s dig into the code.
In the first example, I have created a view by using three state property wrappers. I set a timer on the main thread that should publish every two seconds. The loadingDots() function turns all booleans to true and then they turn back to false after 1.5 seconds thanks to the DisPatchQueue.
The onReceive modifier performs the action we have set in the reloadScreen. Meaning that the screen will reload every 2 seconds to trigger the onAppear modifier. So the screen starts over to create our animation.
If you copy the code from example 1, you’ll see that the three dots appear in a sequence. But then it starts over with all dots still visible. This is not what we want (or at least not what I intended to achieve :p), we want the dots to appear in a sequence, one by one. Then they shall all disappear and the animation then starts over.
If anyone knows why this is happening, please reach out.
Code example 1 - Animation modifier with three property wrappers:
struct LoadingDots: View {
@State private var loadingFirstDot: Bool = false
@State private var loadingSecondDot: Bool = false
@State private var loadingThirdDot: Bool = false
let reloadScreen = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
HStack(spacing: 3) {
Text("Loading")
.font(.largeTitle)
HStack(spacing: 5) {
Rectangle()
.frame(width: 7, height: 7)
.offset(y: 9)
.opacity(loadingFirstDot ? 1 : 0)
.animation(.linear(duration: 0.5), value: loadingFirstDot)
Rectangle()
.frame(width: 7, height: 7)
.offset(y: 9)
.opacity(loadingSecondDot ? 1 : 0)
.animation(.linear(duration: 0.5).delay(0.5), value: loadingSecondDot)
Rectangle()
.frame(width: 7, height: 7)
.offset(y: 9)
.opacity(loadingThirdDot ? 1 : 0)
.animation(.linear(duration: 0.5).delay(1), value: loadingThirdDot)
}
}
}
.onReceive(reloadScreen, perform: { _ in
loadingTheDots()
})
.onAppear {
loadingTheDots()
}
}
func loadingTheDots() {
loadingFirstDot = true
loadingSecondDot = true
loadingThirdDot = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
loadingFirstDot = false
loadingSecondDot = false
loadingThirdDot = false
}
}
}
struct LoadingDots_Previews: PreviewProvider {
static var previews: some View {
LoadingDots()
}
}
After playing around with the code I realized that you can achieve the same (still not what we want though) result by using one property wrapper. As you can see, if you try out code example number 2, is that it behaves the same way, but with less code at least. Always something :)
Code example 2 - Animation modifier with one property wrapper:
struct LoadingDots: View {
@State private var loadingDot: Bool = false
let reloadScreen = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
HStack(spacing: 3) {
Text("Loading")
.font(.largeTitle)
HStack(spacing: 5) {
Rectangle()
.frame(width: 7, height: 7)
.offset(y: 9)
.opacity(loadingDot ? 1 : 0)
.animation(.linear(duration: 0.5), value: loadingDot)
Rectangle()
.frame(width: 7, height: 7)
.offset(y: 9)
.opacity(loadingDot ? 1 : 0)
.animation(.linear(duration: 0.5).delay(0.5), value: loadingDot)
Rectangle()
.frame(width: 7, height: 7)
.offset(y: 9)
.opacity(loadingDot ? 1 : 0)
.animation(.linear(duration: 0.5).delay(1), value: loadingDot)
}
}
}
.onReceive(reloadScreen, perform: { _ in
loadingTheDots()
})
.onAppear {
loadingTheDots()
}
}
func loadingTheDots() {
loadingDot = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
loadingDot = false
}
}
}
struct LoadingDots_Previews: PreviewProvider {
static var previews: some View {
LoadingDots()
}
}
So (as many times before) I needed to turn to withAnimation to get this to behave as I wanted. And voila! In code example 3 we get the behavior that we wanted (or at least the behavior I wanted). What do you think?
Code example 3 - withAnimation:
struct LoadingDots: View {
@State private var loadingFirstDot: Bool = false
@State private var loadingSecondDot: Bool = false
@State private var loadingThirdDot: Bool = false
let reloadScreen = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
HStack(spacing: 3) {
Text("Loading")
.font(.largeTitle)
HStack(spacing: 5) {
Rectangle()
.frame(width: 7, height: 7)
.offset(y: 9)
.opacity(loadingFirstDot ? 1 : 0)
Rectangle()
.frame(width: 7, height: 7)
.offset(y: 9)
.opacity(loadingSecondDot ? 1 : 0)
Rectangle()
.frame(width: 7, height: 7)
.offset(y: 9)
.opacity(loadingThirdDot ? 1 : 0)
}
}
}
.onReceive(reloadScreen, perform: { _ in
loadingTheDots()
})
.onAppear {
loadingTheDots()
}
}
func loadingTheDots() {
withAnimation(.linear(duration: 0.5)) {
loadingFirstDot = true
}
withAnimation(.linear(duration: 0.5).delay(0.5)) {
loadingSecondDot = true
}
withAnimation(.linear(duration: 0.5).delay(1)) {
loadingThirdDot = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
loadingFirstDot = false
loadingSecondDot = false
loadingThirdDot = false
}
}
}
struct LoadingDots_Previews: PreviewProvider {
static var previews: some View {
LoadingDots()
}
}
Alright, to be honest it took me almost an hour of investigation to get this to work, mainly because I had my reloadScreen set to 1.5 seconds, it has always worked before to have the timer and DispatchQueue set to the same time interval. But now it causes problems for the animation. I do not know why exactly, my best guess is that it reloads too fast for the view. Change reloadScreen to 1.5 seconds and see for yourself. You can also see what I mean by checking out the link to my twitter post about this.
let reloadScreen = Timer.publish(every: 1.5, on: .main, in: .common).autoconnect()

That was it for this time! What did you think about this post? If you have any suggestions on how to make the code cleaner or how to make the animation look nicer, please share your thoughts by leaving a comment here or on twitter.
Thanks again for reading. Have a nice day!
Yours truly,
Mr SwiftUI