NEW: Subscribe to Hacking with Swift+ and accelerate your learning! >>

Triggering events repeatedly using a timer

Paul Hudson    @twostraws   

iOS comes with a built-in Timer class that lets us run code on a regular basis. This uses a system of publishers that comes from an Apple framework called Combine. We’ve actually been using parts of Combine for many apps in this series, although it’s unlikely you noticed it. For example, both the @Published property wrapper and ObservableObject protocols both come from Combine, but we didn’t need to know that because when you import SwiftUI we also implicitly import parts of Combine.

Apple’s core system library is called Foundation, and it gives us things like Data, Date, NSSortDescriptor, UserDefaults, and much more. It also gives us the Timer class, which is designed to run a function after a certain number of seconds, but it can also run code repeatedly. Combine adds an extension to this so that timers can become publishers, which are things that announce when their value changes. This is where the @Published property wrapper gets its name from, and timer publishers work the same way: when your time interval is reached, Combine will send an announcement out containing the current date and time.

The code to create a timer publisher looks like this:

let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

That does several things all at once:

  1. It asks the timer to fire every 1 second.
  2. It says the timer should run on the main thread.
  3. It says the timer should run on the common run loop, which is the one you’ll want to use most of the time. (Run loops lets iOS handle running code while the user is actively doing something, such as scrolling in a list.)
  4. It connects the timer immediately, which means it will start counting time.
  5. It assigns the whole thing to the timer constant so that it stays alive.

If you remember, back in project 7 I said “@Published is more or less half of @State” – it sends change announcements that something else can monitor. In the case of regular publishers like this one, we need to catch the announcements by hand using a new modifier called onReceive(). This accepts a publisher as its first parameter and a function to run as its second, and it will make sure that function is called whenever the publisher sends its change notification.

For our timer example, we could receive its notifications like this:

Text("Hello, World!")
    .onReceive(timer) { time in
        print("The time is now \(time)")
    }

That will print the time every second until the timer is finally stopped.

Speaking of stopping the timer, it takes a little digging to stop the one we created. You see, the timer property we made is an autoconnected publisher, so we need to go to its upstream publisher to find the timer itself. From there we can connect to the timer publisher, and ask it to cancel itself. Honestly, if it were’t for code completion this would be rather hard to find, but here’s how it looks in code:

self.timer.upstream.connect().cancel()

For example, we could update our existing example so that it fires the timer only five times, like this:

struct ContentView: View {
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    @State private var counter = 0

    var body: some View {
        Text("Hello, World!")
            .onReceive(timer) { time in
                if self.counter == 5 {
                    self.timer.upstream.connect().cancel()
                } else {
                    print("The time is now \(time)")
                }

                self.counter += 1
            }
    }
}

Before we’re done, there’s one more important timer concept I want to show you: if you’re OK with your timer having a little float, you can specify some tolerance. This allows iOS to perform important energy optimization, because it can fire the timer at any point between its scheduled fire time and its scheduled fire time plus the tolerance you specify. In practice this means the system can perform timer coalescing: it can push back your timer just a little so that it fires at the same time as one or more other timers, which means it can keep the CPU idling more and save battery power.

As an example, this adds half a second of tolerance to our timer:

let timer = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()

If you need to keep time strictly then leaving off the tolerance parameter will make your timer as accurate as possible, but please note that even without any tolerance the Timer class is still “best effort” – the system makes no guarantee it will execute precisely.

Hacking with Swift is sponsored by NSSpain

SPONSORED Announcing NSSpain 2020: Remote Edition! An online, continuous conference for iOS developers. We’ll start on Thursday and finish on Friday, with talks, activities, and lots of fun for 36 hours, non-stop. Sound good? Join us!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.9/5

Link copied to your pasteboard.