Hello SwiftUI Learners,
Welcome back to another issue of Learning SwiftUI, last week I mentioned that we talked about best practices to achieve best performance for the apps we develop at Harmony. One thing we talked about is that we should avoid using nesting if statements. As this adds to the complexity for your project and can lead to that your project won’t compile. Something that we are now implementing for our next update on Harmony Journal. If you want to check it out, press the link below.
So let’s discuss this by replicating a light version of the tweet counter on X (formerly known as Twitter). As you know, when you write a tweet/post on X there is a counter that tracks the characters so you don’t go over 280. Then at the end of this post I will explain why we don’t want the nested if statements and what solution we can use instead.
If you want to learn more about doing this type of progress view, I made a post about this a while back using the Combine protocol, you can check it out here: Twitter Post Progress View
Now let’s do some coding.
First we will go for the good old 140 character limit, because why not. So create a new Swift file with the following code.
struct TweetCharacterLimit {
static let maximumTweetCount = 140
}
This will track help us track the progress on the characters we insert in a text field. Next up we need to create a new SwiftUI file called TweetCounter.
struct TweetCounter: View {
let tweetCount: Int
var percentage: Double {
return Double(tweetCount) / Double(TweetCharacterLimit.maximumTweetCount )
}
var body: some View {
ZStack {
Circle()
.stroke(style: StrokeStyle(lineWidth: 4))
.foregroundColor(.gray)
.opacity(0.4)
.frame(width: 60)
Circle()
.trim(from: 0, to: percentage)
.stroke(style: StrokeStyle(lineWidth: 4))
.rotationEffect(Angle(degrees: -90))
.frame(width: 60)
}
}
}
#Preview {
TweetCounter(tweetCount: 0)
}
As you can see in the simulator, it doesn’t do much at the moment. We have added a constant called tweetCount that will track the characters later on. We also set up a variable that will calculate the written characters divided by the maximum set value we have in our TweetCharacterLimit struct. We use this variable (of type Double) in the trim modifier on the top circle in the ZStack as this will work as our progress tracker. If you change the tweetCount value from 0 to 100 in the preview. You will see what I mean.
Next up, we need to set up our main view that will contain our progress circle and a text editor. So create a new SwiftUI file called MainViewTweetCounter and add the following code:
struct MainViewTweetCounter: View {
@State private var writtenTweetText: String = ""
var body: some View {
VStack(spacing: 25) {
TextEditor(text: $writtenTweetText)
.frame(height: 300)
.scrollContentBackground(.hidden)
.padding()
.background(
Color.gray.opacity(0.2)
.cornerRadius(20)
)
TweetCounter(tweetCount: writtenTweetText.count)
}
.padding()
}
}
#Preview {
MainViewTweetCounter()
}
As seen in the code and video, we added a state property wrapper to track the characters and applied it to our tweetCount constant in the TweetCounter view. The progress circle is working fine, now we want to add some colors to it that will change based on the number of characters written.
We can solve this by using nested if statements, and then I will explain why it is bad and what we should use instead to gain better performance in the app.
Add this code to the TweetCounter struct:
struct TweetCounter: View {
let tweetCount: Int
var percentage: Double {
return Double(tweetCount) / Double(TweetCharacterLimit.maximumTweetCount )
}
var body: some View {
ZStack {
Circle()
.stroke(style: StrokeStyle(lineWidth: 4))
.foregroundColor(.gray)
.opacity(0.4)
.frame(width: 60)
if tweetCount < 120 {
Circle()
.trim(from: 0, to: percentage)
.stroke(style: StrokeStyle(lineWidth: 4))
.rotationEffect(Angle(degrees: -90))
.frame(width: 60)
.foregroundColor(.blue)
} else if tweetCount < 140 {
Circle()
.trim(from: 0, to: percentage)
.stroke(style: StrokeStyle(lineWidth: 4))
.rotationEffect(Angle(degrees: -90))
.frame(width: 60)
.foregroundColor(.yellow)
} else {
Circle()
.trim(from: 0, to: percentage)
.stroke(style: StrokeStyle(lineWidth: 4))
.rotationEffect(Angle(degrees: -90))
.frame(width: 60)
.foregroundColor(.red)
}
}
}
}
#Preview {
TweetCounter(tweetCount: 0)
}
Now we get the behavior we want, once the character limit hits 120, the color will change to yellow and once it reaches 140 it will change to red. This might not be the best example as you can achieve this with a ternary operator instead of using if else statements. But it will prove my point as it is valid for any type of nested if statements in your project. If you want to learn how to do it with ternary operators, I have post about it here: Multiple Options for Ternary Operators.
The reason you don’t want to go for this solution is because when you set it up with nesting if statements, it means the compiler has to check multiple conditions. Which means it has to check “is this true? no, lets check if this is true, no this wasn’t true either. But this third one is true. Great!”. Which means it takes longer to check. In Swift and SwiftUI, the code runs on your device, and we want it to be as fast as possible. By keeping the code simple and avoiding too many nested if statements, we help the compiler to do its job quickly and efficiently.
Instead of nesting multiple if statements, you can use switch statements instead to make your code more readable and avoid deep nesting.
Replace the code in the TweetCounter struct with this code instead.
struct TweetCounter: View {
let tweetCount: Int
var tweetProgressColor: Color {
switch tweetCount {
case 0...119:
return .blue
case 120...139:
return .yellow
case 140...:
return .red
default:
return .gray
}
}
var percentage: Double {
return Double(tweetCount) / Double(TweetCharacterLimit.maximumTweetCount )
}
var body: some View {
ZStack {
Circle()
.stroke(style: StrokeStyle(lineWidth: 4))
.foregroundColor(.gray)
.opacity(0.4)
.frame(width: 60)
Circle()
.trim(from: 0, to: percentage)
.stroke(style: StrokeStyle(lineWidth: 4))
.rotationEffect(Angle(degrees: -90))
.frame(width: 60)
.foregroundColor(tweetProgressColor)
}
}
}
#Preview {
TweetCounter(tweetCount: 0)
}
And voila! The code will work in the same way but now it is easier to read and it will perform better as it will take less work to compile. Since this is a small project, both ways will work and you won’t notice any difference, but these things add up and while your project grows, it increases the risk of performance issues.
You should always strive for the best code as possible in your projects. But then again, I’d like to point out, don’t break it if it works.
That was it for this weeks post, hope you learned something new, I know I did. If you have any comments on this and other best practices, please share them in comment section.
Have a nice day and see you all next Sunday.
/Mr SwiftUI