Hello SwiftUI learners,
Welcome to a new issue of Learning SwiftUI. During the week I have been quite busy with my goals for 2024. I also had some interesting meetings with the team at Harmony Apps where we went through the best practices we want to use in our apps. We currently have one app on the App Store, it’s a journal app and you can find it here:
Let’s dig into this week’s issue, as you may know. Apple has a built in progress view that you can use in your projects to simulate for the user that the app is loading or fetching data.
You can simply call it with very little code.
struct CustomProgressView: View {
var body: some View {
VStack {
ProgressView()
}
}
}
Simple as that, this is good to use once you start building your apps. Actually, it is good to use whenever, but sometimes you might want to build your own progress view instead, to customize your app a bit more.
So let’s start doing that, in this issue we will build a simple custom progress view with increasing circles sizes to create a nice effect. I might come back to this topic later on as I really like doing these. Let’s start by adding this code to your project.
struct CustomProgressView: View {
var body: some View {
VStack {
ZStack {
ForEach(1...20, id: \.self) { index in
Circle()
}
}
}
}
}
As you can see in the above code and picture, this doesn’t do much. What we have done is to create an ForEach that takes 20 different items. Right now we have only one circle visible. What we want to do is to make that circle into 20 small ones. Here is how we can do that.
struct CustomProgressView: View {
var body: some View {
VStack {
ZStack {
ForEach(1...20, id: \.self) { index in
let degress = CGFloat(index) * 18 // we multiply it 18 times as 20x18 = 360 which will make the circles into a big circle.
let width = CGFloat(index) + 1 // this will increase the size of each circle by 1.
Circle()
.frame(width: 12 + width)
.offset(x: 150)
.foregroundColor(.green)
.rotationEffect(Angle(degrees: degress))
}
}
}
}
}
This is much better, what we have done now is that we added two constants (degrees and width). We added a frame to the circles that starts at 12 and will increase by 1 for each circle. We used our degrees constant in a rotation effect modifier so all 20 circles is spread across an 360 degree circle area (20x18). We offset the circles 150px from the center so we get a smooth circle. If we don’t offset, the circle will stack on top of each other. Comment out the offset modifier or set the offset to 100 and you’ll see what I mean.
Next up we want the circles to rotate, for this we need to add another rotation effect, an animation and a button.
struct CustomProgressView: View {
@State private var loading: Bool = true
var body: some View {
VStack {
Spacer()
ZStack {
ForEach(1...20, id: \.self) { index in
let degress = CGFloat(index) * 18 // we multiply it 18 times as 20x18 = 360 which will make the circles into a big circle.
let width = CGFloat(index) + 1 // this will increase the size of each circle by 1.
Circle()
.frame(width: 12 + width)
.offset(x: 150)
.foregroundColor(.green)
.rotationEffect(Angle(degrees: degress))
.rotationEffect(Angle(degrees: loading ? -360 : 0))
.animation(.linear(duration: 2).repeatForever(autoreverses: false), value: loading)
}
}
Spacer()
Button(action: {
loading.toggle()
}, label: {
Text("Rotate")
.font(.largeTitle)
})
}
}
}
So that is the custom progress view for you. We added a bool property wrapper to track it for changes. Notice I set the bool to true from the beginning. Normally I do not do this, but I thought it would be a fun thing for this topic. Anyhow, once we press the button it will start rotating the circles. We set the rotation to -360 because we want it to rotate clock wise. If you change it to just 360, it will rotate in the other direction (if you set the bool to false as default value, then you have to set the degrees as 360 (and not -360) to get clockwise direction. You can also play around with the time for the animation if you want it to go faster or slower. I’ll leave that up to you.
Now you might be thinking, it’s quite big. How can I use it in a project. And you are right. Let’s create a small scenario where we can use this custom progress view. Just keep in mind, that the UI is not in focus in the below code and example.
struct CustomProgressView: View {
@State private var loading: Bool = false
@State private var removeText: Bool = false
@State private var showText: Bool = false
var body: some View {
VStack {
Spacer()
ZStack {
RoundedRectangle(cornerRadius: 10)
.frame(width: 350, height: 200)
Text("Cool App")
.foregroundStyle(.white)
.font(.largeTitle)
.fontWeight(.semibold)
.opacity(removeText ? 0 : 1)
ZStack {
ForEach(1...20, id: \.self) { index in
let degress = CGFloat(index) * 18 // we takes it times 18 as 20x18 = 360 which will make the circles into a big circle.
let width = CGFloat(index) / 2 // this will increase the size of each circle by 1.
Circle()
.frame(width: width)
.offset(x: 40)
.foregroundColor(.green)
.rotationEffect(Angle(degrees: degress))
.rotationEffect(Angle(degrees: loading ? 360 : 0))
.animation(.linear(duration: 1).repeatForever(autoreverses: false), value: loading)
}
}
.opacity(loading ? 1 : 0)
Text("Welcome to the future")
.foregroundStyle(.white)
.font(.title)
.opacity(showText ? 1 : 0)
.animation(.linear(duration: 1).delay(3), value: showText)
}
Spacer()
Button(action: {
loading.toggle()
removeText = true
showText.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
loading.toggle()
})
}, label: {
Text("Join ->")
.font(.largeTitle)
})
}
}
}
This is one way you can implement a custom progress view to your project. In this case, we present it once we go between two different views. To achieve this, we added two more property wrappers so we can dismiss the cool app text view and then appear the second text view with an animation.
Notice I switched our first state property wrapper to false, which means we have to change the rotation effect from -360 to 360 instead if we want it to spin the same direction as before (as explained before).
We also changed the width constant from adding 1 to the index and instead we divide it by two. This way the circles get smaller. Then we had to change the offset from 150 to 40 to make it proportional.
We also had to add a dispatch queue to the button so we can toggle back the loading boolean to false after three seconds. Which means that the progress view will disappear after three seconds. And we set an delay to the animation of the second text view so it will appear once the progress view is gone.
I really recommend you to play around with this code and see if you can improve it or make something different. You might even try it in on of your projects, that would be sweet!
I hope you enjoyed this weeks post, leave a comment if you have any suggestions for improving any type of progress view.
Have a nice day!
/Mr SwiftUI