Why is SwiftUI so fun? (part 2)
This is the second part of my dive into SwiftUI and what makes it in my opinion the best way to create a mobile app.
Introduction
Last week I published a blog post about why I like SwiftUI. I hope you had the chance to give it a read (if not, here it is :D). We basically built a simple watchOS counter app with the purpose of showcasing just how easy it is to grasp the basics of SwiftUI, especially if you come from a web development background like me.
Well, in today's blog post we will be expanding on last week's post, and we will be adding a timer to our watchOS app in the most simple way possible.
Our app will work like this:
- We can increment the counter with the
+
icon - When we tap
Go
, the counter will start counting down by 1 every second. - When counter reaches
0
, it will stop.
Prerequisites
- Part 1: eduard.hashnode.dev/how-easy-is-swift-ui
- Basic OOP knowledge
Implementing the Timer
We want to use some Swift magic so that every second, we will run a piece of code that will decrement the counter. In JavaScript, you'd be doing this using a setInterval
method. In SwiftUI, we have the same approach, but it might look a bit different.
To get this done, we'll be using the Timer class that is already build into Swift.
Let's declare our timer state variable like you see below:
@State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
I know, this looks a bit odd. We can probably figure out that we're running this every: 1
second, but what about the other arguments, what do does mean? Let's break them all down:
- As we just mentioned,
every: 1
basically means that you want some stuff to happen every second on: .main
is how we tell Swift that we want our main UI to be updated as state changesin: .common
will allow the timer to run alongside other common tasks in the app such as scrolling.autoconnect()
means that we will initialize the timer as soon as the main component loads.
Ok, so now that we have the Timer
foundation in place and we understand how it works, it's time to actually do something with it. To make use of our timer state variable, we need to use the onRecieve
modifier on any UI element that we want to change every second.
On the Text UI Element from the last week's code, we are going to pass in our timer
state variable to the onReceive
modifier, and we will (for now) just decrement our counter state variable by 1 every second. If the counter is 0 or less, we will set the counter to 0 every second ๐ฌ - we will solve this later.
@State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
...
Text("\(count)")
.font(.system(size: 90))
.fontWeight(.black)
.multilineTextAlignment(.center)
.onReceive(timer) { _ in
count = count > 0 ? count - 1 : 0
}
Great, we will come back to address some issues here, but for now we will move on to figuring out how to trigger this .onReceive
modifier.
The plan is to start the countdown timer as soon as we press on the Go!
button, so all we will need to do is assign our timer state variable a new instance of the Timer
built-in class with the same arguments mentioned earlier. It will look like this:
Button {
timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
} label: {
Text("Go!")
.font(.system(size: 34))
}
Awesome job. At a surface level, if you would build the app now, it will run ok, however there are some pretty important performance issues that we need to work on before we finish.
Dealing with problems
Okay, let's first see what the problems are in our code, then we can go ahead and see how we can tackle them one by one:
- As mentioned above, when our counter hits
0
, we will set thecount
to0
every second. We are never actually stopping the Timer. - If we never increment the counter, we can still press on the
Go!
button.
For the first issue we have, our onReceive
modified needs a bit of validation, and we also need to cancel our Timer
as part of that validation. Basically, we will check if the counter is equal to 0
, and if that is true, we will cancel the Timer like so: timer.upstream.connect().cancel()
Here is how our updated onReceive
modified looks like:
.onReceive(timer) { _ in
if count == 0 {
timer.upstream.connect().cancel()
}
count = count > 0 ? count - 1 : 0
}
Now for the last adjustment, we want to allow the button to be pressed only if the counter is greater than 0
, and this can be done very easily by adding the disabled
modifier to the Go!
button. Our code will look like this:
Button {
timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
} label: {
Text("Go!")
.font(.system(size: 34))
}
.disabled(count == 0)
If you need to have a look at the whole code block, feel free to have a look at the gist here.
Closing Notes
In this final part of the SwiftUI blog posts, I hope I was able to make you a little more excited about the things you can do with SwiftUI super easily. While my expertise is in web development, I find it incredibly inviting to start developing apps with SwiftUI and I am looking forward to dive even deeper into SwiftUI in a future blog post.
See you on the next one โ๐ผ