Good day fellow SwiftUI learner,
This week has been pretty slow for me, as I am trying to figure out what I wanna do next. I have started work on my sport betting app at least, but time and motivation has been somewhat low for the last couple of days. However, I tried playing around with some animations during the week. My first plan was to have a ball bouncing in but I ended up with the example I am gonna present to you in this post instead.
To be honest, I guess this can only be seen as some inspiration for something else. Because there is now use case for this particular animation (I think). My goal was to build a “cool” onboarding screen or something similar. But it ended up kind of bad. But hey, this newsletter is not only about the fancy stuff.
So let’s go through the example for this week. For this post, I will not link to a video on my twitter as I noticed that almost no one clicks them anyway.
And as already stated, I wanted to have a ball bounce in on three ledges. But I did not have the time to figure out how to make it move smoothly, so this is what I came up with instead.
First we will need to set up three different rectangles that will change color once our “ball” hits them. And we will also set up two dummy circles that we have trimmed. Just to see where we want the ball to go. To be honest, this is a tacky solution as I have used trial and error to position them instead of math :)
struct BouncingBall: View {
@State private var greenRectangle: Bool = false
@State private var purpleRectangle: Bool = false
var body: some View {
VStack {
ZStack {
Circle()
.trim(from: 0.45, to: 1.0)
.stroke(style: StrokeStyle(lineWidth: 10))
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: -70, y: 70)
.foregroundColor(.gray.opacity(0.3))
Circle()
.trim(from: 0.45, to: 1)
.stroke(style: StrokeStyle(lineWidth: 10))
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: 70, y: 70)
.foregroundColor(.gray.opacity(0.3))
}
HStack(spacing: 30) {
Rectangle()
.frame(width: 100)
.foregroundColor(.blue)
Rectangle()
.frame(width: 100)
.foregroundColor(greenRectangle ? .green : .gray)
.animation(.linear.delay(1.5), value: greenRectangle)
Rectangle()
.frame(width: 100)
.foregroundColor(purpleRectangle ? .purple : .gray)
.animation(.linear.delay(3), value: purpleRectangle)
}
.frame(height: 20)
.padding(.horizontal)
}
}
}
This is what your canvas should look like at the moment. At the end of this post, we will remove these two circles. As you may have noticed, we have added two state property wrappers that will change the color of the second and third rectangle. We will use them later on.
Want to learn more about SwiftUI animations?
I created a book for anyone new to SwiftUI that wants to develop their SwiftUI animation skills. The book cover the basics and more advance techniques to animating objects and views in SwiftUI. You can check it out by clicking the link below:
Learning SwiftUI Animations - The Beginner Roadtrip
Next we need to add two more circles within the ZStack and trim them to be smaller, this will be the ones that we will move later on. I also added four state property wrappers as CGFloats and put them in the trim modifier. We need two of them for each circle so we can move them simultaneously to keep the size for the trim. You code should now look like this:
struct BouncingBall: View {
@State private var moveFirst: CGFloat = 0.45
@State private var moveSecond: CGFloat = 0.47
@State private var moveSecondFirst: CGFloat = 0.45
@State private var moveSecondSecond: CGFloat = 0.47
@State private var greenRectangle: Bool = false
@State private var purpleRectangle: Bool = false
var body: some View {
VStack {
ZStack {
Circle()
.trim(from: 0.45, to: 1.0)
.stroke(style: StrokeStyle(lineWidth: 10))
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: -70, y: 70)
.foregroundColor(.gray.opacity(0.3))
Circle()
.trim(from: 0.45, to: 1)
.stroke(style: StrokeStyle(lineWidth: 10))
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: 70, y: 70)
.foregroundColor(.gray.opacity(0.3))
Circle()
.trim(from: moveFirst, to: moveSecond)
.stroke(style: StrokeStyle(lineWidth: 10))
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: -70, y: 70)
Circle()
.trim(from: moveSecondFirst, to: moveSecondSecond)
.stroke(style: StrokeStyle(lineWidth: 10))
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: 70, y: 70)
}
HStack(spacing: 30) {
Rectangle()
.frame(width: 100)
.foregroundColor(.blue)
Rectangle()
.frame(width: 100)
.foregroundColor(greenRectangle ? .green : .gray)
.animation(.linear.delay(1.5), value: greenRectangle)
Rectangle()
.frame(width: 100)
.foregroundColor(purpleRectangle ? .purple : .gray)
.animation(.linear.delay(3), value: purpleRectangle)
}
.frame(height: 20)
.padding(.horizontal)
}
}
}
As you can see, this does not look that good as our new trimmed circles “stick up” over the rectangles. The reason for this is that there is some spacing in our VStack, so we need to set it to zero. We can also add some movement to our small trimmed circles. In this case I really like to use the onAppear modifier. So let’s add this next.
struct BouncingBall: View {
@State private var moveFirst: CGFloat = 0.45
@State private var moveSecond: CGFloat = 0.47
@State private var moveSecondFirst: CGFloat = 0.45
@State private var moveSecondSecond: CGFloat = 0.47
@State private var greenRectangle: Bool = false
@State private var purpleRectangle: Bool = false
var body: some View {
VStack(spacing: 0) {
ZStack {
Circle()
.trim(from: 0.45, to: 1.0)
.stroke(style: StrokeStyle(lineWidth: 10))
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: -70, y: 70)
.foregroundColor(.gray.opacity(0.3))
Circle()
.trim(from: 0.45, to: 1)
.stroke(style: StrokeStyle(lineWidth: 10))
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: 70, y: 70)
.foregroundColor(.gray.opacity(0.3))
Circle()
.trim(from: moveFirst, to: moveSecond)
.stroke(style: StrokeStyle(lineWidth: 10))
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: -70, y: 70)
Circle()
.trim(from: moveSecondFirst, to: moveSecondSecond)
.stroke(style: StrokeStyle(lineWidth: 10))
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: 70, y: 70)
}
HStack(spacing: 30) {
Rectangle()
.frame(width: 100)
.foregroundColor(.blue)
Rectangle()
.frame(width: 100)
.foregroundColor(greenRectangle ? .green : .gray)
.animation(.linear.delay(1.5), value: greenRectangle)
Rectangle()
.frame(width: 100)
.foregroundColor(purpleRectangle ? .purple : .gray)
.animation(.linear.delay(3), value: purpleRectangle)
}
.frame(height: 20)
.padding(.horizontal)
}
.onAppear {
withAnimation(.linear(duration: 1.5)) {
moveFirst = 0.98
moveSecond = 1.0
}
withAnimation(.linear(duration: 1.5).delay(1.5)) {
moveSecondFirst = 0.98
moveSecondSecond = 1.0
}
}
}
}
This will move the trimmed circles. Last but not least, let’s remove the two dummy circles as we only used them for alignment in the first place. And let’s also add some color changes. For this we need to add two more state property wrappers that can change the color of the moving pieces. Then we also need to change the two boolean states to change the color on the rectangles that we added in the beginning of this post. The final code should now look like this:
struct BouncingBall: View {
@State private var moveFirst: CGFloat = 0.45
@State private var moveSecond: CGFloat = 0.47
@State private var moveSecondFirst: CGFloat = 0.45
@State private var moveSecondSecond: CGFloat = 0.47
@State private var changeFirstColor: Bool = false
@State private var changeSecondColor: Bool = false
@State private var greenRectangle: Bool = false
@State private var purpleRectangle: Bool = false
var body: some View {
VStack(spacing: 0) {
ZStack {
Circle()
.trim(from: moveFirst, to: moveSecond)
.stroke(style: StrokeStyle(lineWidth: 10))
.foregroundColor(changeFirstColor ? .green : .blue)
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: -70, y: 70)
Circle()
.trim(from: moveSecondFirst, to: moveSecondSecond)
.stroke(style: StrokeStyle(lineWidth: 10))
.foregroundColor(changeSecondColor ? .purple : .green)
.rotationEffect(Angle(degrees: 13), anchor: .center)
.frame(width: 130)
.offset(x: 70, y: 70)
}
HStack(spacing: 30) {
Rectangle()
.frame(width: 100)
.foregroundColor(.blue)
Rectangle()
.frame(width: 100)
.foregroundColor(greenRectangle ? .green : .gray)
.animation(.linear.delay(1.5), value: greenRectangle)
Rectangle()
.frame(width: 100)
.foregroundColor(purpleRectangle ? .purple : .gray)
.animation(.linear.delay(3), value: purpleRectangle)
}
.frame(height: 20)
.padding(.horizontal)
}
.onAppear {
withAnimation(.linear(duration: 1.5)) {
moveFirst = 0.98
moveSecond = 1.0
}
withAnimation(.linear.delay(0.5)) {
changeFirstColor = true
}
withAnimation(.linear(duration: 1.5).delay(1.5)) {
moveSecondFirst = 0.98
moveSecondSecond = 1.0
}
withAnimation(.linear.delay(2)) {
changeSecondColor = true
}
greenRectangle = true
purpleRectangle = true
}
}
}
And there you have it, maybe not the nicest animation you’ll ever see. But I hope you can use this as inspiration to create some nice animations in the future. Personally I also like to combine the animation modifier with withAnimation when it is possible. But as a rule, you should always use withAnimation as it is more reliable. If you ever end up getting strange behavior with the animation modifier, you should try running it with withAnimation as well as it is way better to distinguish the state changes in the right order.
Have a nice day and see you next week!
Mr SwiftUI