Leave a rating/review
Notes: 15. Using Combine
Make sure the course server is running and continue with your project from the previous episode or open the starter project for this episode.
Using Combine: Timer
In this episode, you’ll add a timer, to show how long a download is taking. You’ll use Apple’s Timer class, which is much easier to use, now that it has a Combine publisher.
Combine is Apple’s reactive programming framework. It’s a huge topic, with its own book and video courses. You’ll find links to these below this video window.
The basic Combine concepts are publishers and subscribers.
A Combine publisher asynchronously emits values over time and can complete with success or failure, just like an AsyncSequence.
And it’s easy to use Combine with Swift concurrency.
In DownloadView, import Combine:
import Combine
Add a State property to store the timer task so you can cancel it:
@State var timerTask: Task<Void, Error>?
A timer task should start when a download starts. A good place to detect this is when you store a Task in downloadTask.
Add a didSet accessor to downloadTask:
@State var downloadTask: Task<Void, Error>? 🟩{
didSet {
}
}
Add some housekeeping code in this didSet closure:
timerTask?.cancel()
guard isDownloadActive else { return }
let startTime = Date().timeIntervalSince1970
You cancel any currently running timer task. Then, if there’s a download happening, store the start time so you can calculate the duration.
Now for your Combine timer! You’ll use the Timer static method publish to get a TimerPublisher object, which emits the current date on the interval you specify.
Create a timer sequence:
let timerSequence = Timer
.publish(every: 1, on: .main, in: .common)
You create a TimerPublisher that emits the current date every second, on the Main runloop, in any of the usual modes: default, modal and event tracking. Add another modifier:
let timerSequence = Timer
.publish(every: 1, tolerance: 1, on: .main, in: .common)
🟩
.autoconnect()
Instead of manually connecting to this publisher, you tell it to start ticking automatically whenever someone subscribes to it.
Next, convert the publisher’s value into a duration value:
let timerSequence = Timer
.publish(every: 1, tolerance: 1, on: .main, in: .common)
.autoconnect()
🟩
.map { date -> String in
let duration = Int(date.timeIntervalSince1970 - startTime)
return "\(duration)s"
}
map calculates the elapsed time in seconds and returns the duration as a String.
Finally, and most importantly, you need an AsyncSequence of duration values:
let timerSequence = Timer
.publish(every: 1, tolerance: 1, on: .main, in: .common)
.autoconnect()
.map { date -> String in
let duration = Int(date.timeIntervalSince1970 - startTime)
return "\(duration)s"
}
🟩
.values
values returns an asynchronous sequence of the publisher’s events, which you can loop over as usual. Time to iterate!
Still in the didSet accessor, create a new asynchronous task, store it in timerTask and loop over the sequence:
timerTask = Task {
for await duration in timerSequence {
self.duration = duration
}
}
In fact, you can use for await with any Combine publisher by accessing its values property, which automatically wraps the publisher in an AsyncSequence.
You can test this now, but first, take care of stopping the timer when it shouldn’t be running.
First, make the timer stop when the download ends. In downloadWithUpdatesAction, add this line after you set isDownloadActive to false:
timerTask?.cancel()
Also cancel the timer task when the user taps Cancel Now.
In toolbar {...}, add the same line to the Button action:
timerTask?.cancel()
Build and run. Select a file, then tap Gold. Let the file download completely: The timer stops when the image appears. Go back and start another Gold download, but tap Cancel Now before it completes, and the timer also stops.
Congratulations! You’ve used a Combine publisher to add a download timer to your app.
In the next episode, you’ll implement the premium download plan, Cloud 9, using concurrent partial downloads.