3

Im trying to build a timer which continuing counting down when the app is in the background or even the screen is locked. After the timer reached 0 a notification should been send.

So far it works on the simulator but not on the real device (iPhone X, running iOS 13.5.1). The task simply pauses when entering background.

How do I keep the countdown running on the real device?

import SwiftUI
import UserNotifications

struct ContentView: View {
    @State var start = false
    @State var count = 10 // 10 sec timer
    @State var time = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    var body: some View{
        VStack{
            Text("\(self.count)")
            
            Button(action: {
                self.start.toggle()
            }) {
                Text("Start")
            }
        }
        .onAppear(perform: {
            UNUserNotificationCenter.current().requestAuthorization(options: [.badge,.sound,.alert]) { (_, _) in
            }
        })
            .onReceive(self.time) { (_) in
                if self.start{
                    if self.count != 0{
                        self.count -= 1
                    }
                    else{
                        self.sendNotification()
                    }
                }
        }
    }
    
    func sendNotification(){
        
        let content = UNMutableNotificationContent()
        content.title = "Timer"
        content.body = "Time is up!"
        
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
        let req = UNNotificationRequest(identifier: "MSG", content: content, trigger: trigger)
        
        UNUserNotificationCenter.current().add(req, withCompletionHandler: nil)
    }
}

Selected background Modes (Screenshot)

Daniel
  • 568
  • 3
  • 15
  • 1
    `Timer` doesn't run when your app is suspended. You can use a `Timer` to update your UI, but you can't use a simple counter to track time. You need to calculate the end date for your timer and use that to determine time is up. You could consider scheduling a notification for when the timer expires if there is a chance your app will be suspended when that happens – Paulw11 Jul 10 '20 at 22:45

1 Answers1

4

Apple will not allow timers to run in the background.

For more information see these two questions:

Apple will not allow you to run processes for long periods of time after your app has been backgrounded. From the docs:

Implementing Long-Running Tasks

For tasks that require more execution time to implement, you must request specific permissions to run them in the background without their being suspended. In iOS, only specific app types are allowed to run in the background: ]

  • Apps that play audible content to the user while in the background, such as a music player app
  • Apps that record audio content while in the background
  • Apps that keep users informed of their location at all times, such as a navigation app
  • Apps that support Voice over Internet Protocol (VoIP)
  • Apps that need to download and process new content regularly
  • Apps that receive regular updates from external accessories
  • Apps that implement these services must declare the services they support and use system frameworks to implement the relevant aspects of those services.

Declaring the services lets the system know which services you use, but in some cases it is the system frameworks that actually prevent your application from being suspended.

But in this case you want to send a notification, so I suggest doing something similar but with a twist.

When the user leaves the app, you want to get the current time. You then want to add the remaining timer time to the current time and run code in your app to send a notification at current time + timer remaining time.

Code for sending a notification at a specific time can be viewed here:

let content = UNMutableNotificationContent()
                content.title = "Title"
                content.body = "Body"
                content.sound = UNNotificationSound.default()

                let gregorian = Calendar(identifier: .gregorian)
                let now = Date()
                var components = gregorian.dateComponents([.year, .month, .day, .hour, .minute, .second], from: now)

                // Change the time to 7:00:00 in your locale
                components.hour = 7
                components.minute = 0
                components.second = 0

                let date = gregorian.date(from: components)!

                let triggerDaily = Calendar.current.dateComponents([.hour,.minute,.second,], from: date)
                let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDaily, repeats: true)


                let request = UNNotificationRequest(identifier: CommonViewController.Identifier, content: content, trigger: trigger)
                print("INSIDE NOTIFICATION")

                UNUserNotificationCenter.current().add(request, withCompletionHandler: {(error) in
                    if let error = error {
                        print("SOMETHING WENT WRONG")
                    }
                })