I'm trying to use this cool timer from kavsoft
But when i hide app or turn off screen device (not close app) this timer after a couple of seconds or minutes in the background mode stops counting and stops. If I open the application, it continues to count down again.
I tried on the original code and on my modified one. In mine, I made the format of hours minutes seconds. In any of them, counting in background mode stops working. Is any way to fix it? it may take up to 2 hours for my needs in app to work in background mode. Here is my modifed code in swift
import SwiftUI
import UserNotifications
struct TimerDiscovrView : View {
@State var start = false
@State var to : CGFloat = 0
@State var MaxCount = 0
@State var count = 0
var testTimer: String
var testName: String
@State var time = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View{
ZStack{
//заливка экрана
Color.black.opacity(0.06).edgesIgnoringSafeArea(.all)
VStack{
ZStack{
Circle()
.trim(from: 0, to: 1)
.stroke(Color.black.opacity(0.09), style: StrokeStyle(lineWidth: 35, lineCap: .round))
.frame(width: 280, height: 280)
Circle()
.trim(from: 0, to: self.to)
.stroke(Color.red, style: StrokeStyle(lineWidth: 35, lineCap: .round))
.frame(width: 280, height: 280)
.rotationEffect(.init(degrees: -90))
VStack{
Text("\(valueFormat(mCount: count/3600)):\(valueFormat(mCount: count/60%60)):\(valueFormat(mCount: count%60))")
.font(.system(size: 45))
.fontWeight(.bold)
.padding(.top)
.padding(.bottom)
Text(LocalizedStringKey("Total time")).lineLimit(2)
Text("\(valueFormat(mCount: MaxCount/3600)):\(valueFormat(mCount: MaxCount/60%60)):\(valueFormat(mCount: MaxCount%60))")
.font(.title)
}
}
VStack {
HStack(spacing: 20){
Button(action: {
if self.count == MaxCount {
self.count = 0
withAnimation(.default){
self.to = 0
}
}
self.start.toggle()
}) {
HStack(spacing: 15){
Image(systemName: self.start ? "pause.fill" : "play.fill")
.foregroundColor(.white)
//текст на кнопке
// Text(self.start ? "Pause" : "Play")
// .foregroundColor(.white)
}
.padding(.vertical)
.frame(width: (UIScreen.main.bounds.width / 2) - 55)
.background(Color.red)
.clipShape(Capsule())
.shadow(radius: 6)
}
Button(action: {
self.count = 0
withAnimation(.default){
self.to = 0
}
}) {
HStack(spacing: 15){
Image(systemName: "arrow.clockwise")
.foregroundColor(.red)
//текст на кнопке
// Text("Restart")
// .foregroundColor(.red)
}
.padding(.vertical)
.frame(width: (UIScreen.main.bounds.width / 2) - 55)
.background(
Capsule()
.stroke(Color.red, lineWidth: 2)
)
.shadow(radius: 6)
}
}
Text(LocalizedStringKey("Set timer for")).font(.subheadline).lineLimit(1)
Text("\(testName)").font(.title).lineLimit(2)
VStack {
Text(LocalizedStringKey("Attention")).font(.footnote).foregroundColor(.gray).fixedSize(horizontal: false, vertical: true)
}.padding(.horizontal)
}
.padding(.top)
.padding(.bottom, 30)
}
}.navigationBarTitle(LocalizedStringKey("Set timer"), displayMode: .inline)
.onAppear(perform: {
if self.MaxCount == 0 {
let arrayTimer = testTimer.split(separator: " ")
if arrayTimer.count > 1 {
let counts = Int(arrayTimer[0]) ?? 0
/// Преобразование в секунды
switch arrayTimer[1] {
case "min":
self.MaxCount = counts*60
case "hour":
self.MaxCount = counts*3600
case "hours":
self.MaxCount = counts*3600
default:
self.MaxCount = counts
}
}
}
UNUserNotificationCenter.current().requestAuthorization(options: [.badge,.sound,.alert]) { (_, _) in
}
})
.onReceive(self.time) { (_) in
if self.start{
if self.count != MaxCount {
self.count += 1
print("hello")
withAnimation(.default){
self.to = CGFloat(self.count) / CGFloat(MaxCount)
}
}
else {
self.start.toggle()
self.Notify()
}
}
}
}
func Notify(){
let content = UNMutableNotificationContent()
/// key - ключ_локализованной_фразы, comment не обязательно заполнять
content.title = NSLocalizedString("Perhaps your test is ready", comment: "")
/// с аргументами (key заменяете на нужное)
// Вид локализованной строки в файлах локализации "key %@"="It,s time to check your %@";
content.body = String.localizedStringWithFormat(NSLocalizedString("It's time to check your %@", comment: ""), self.testName)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let req = UNNotificationRequest(identifier: "MSG", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(req, withCompletionHandler: nil)
}
func valueFormat(mCount: Int) -> String {
String(format: "%02d", arguments: [mCount])
}
}
I had this problem before, and I was struggling to fix it, however I didn't get any useful answer. I tried to activate Background Modes
without success.
Here it is how did I overcome this problem:
Let's imagine that your counter has started counting, the counter now is at 5 seconds
, then suddenly the user enter the background what do we need to do?
Let's go to SceneDelegate
and declare those variables
var appIsDeadAt: Double?
var appIsBackAliveAt: Double?
We need to save the time when the user enter the background in sceneDidEnterBackground
appIsDeadAt = Date().timeIntervalSince1970
When user enter the application again, we need to calculate the time that the application stayed in background. Go to sceneWillEnterForeground
and get the time when the application is back active
appIsBackAliveAt = Date().timeIntervalSince1970
Now we need to calculate the total time that the application stayed in the background
let finalTime = (appIsBackAliveAt! - appIsDeadAt!)
Finally, let's say that the application stayed in background for 10 seconds
and the previous time is 5
by that 5 + finalTime
(Which is 10 seconds) the total time would be 15 seconds
, and then update your counter time to continue counting from 15 seconds
.
Note: use UserDefaults
to pass the values to your counter, to make it easy for you.
One real time example is from my own application that is released on applestore: Chatiw
Basically of my timer, is when the user watch an Ads video I'll reward him with a 300 seconds
without ads.
After implementing all the steps above, when I have the final time I'll store it on UserDefaults
:
userDefaults.setValue(finalTime.rounded(), forKey: "timeInBg")
Then on my main code that contain the counter I'll update the counter to the new time:
adsRemovalCounter = adsRemovalCounter - Int(self.userDefaults.double(forKey: "timeInBg"))
After that I'll delete the UserDefaults
key for the finalTime
so it won't interfere with the next calculation when the user enter the background again.
self.userDefaults.removeObject(forKey: "timeInBg")
Here it is an example in SwiftUI
that I just made:
ContentView
File:
import SwiftUI
struct ContentView: View {
@ObservedObject var counterService = CounterService()
var body: some View {
VStack {
Text("\(self.counterService.counterTime)")
.font(.largeTitle)
.fontWeight(.bold)
.foregroundColor(Color.white)
.frame(width: 100, height: 80)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.shadow(color: Color.red.opacity(0.5), radius: 10, x: 5, y: 2)
.padding()
.padding(.bottom, 100)
Button(action: {
self.counterService.startCounting()
}) {
Text("Start Counter")
.font(.title)
.fontWeight(.bold)
.foregroundColor(Color.white)
.frame(width: 200, height: 80)
.background(Color.gray)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.shadow(color: Color.black.opacity(0.5), radius: 10, x: 5, y: 2)
}
}
}
}
CounterService
File:
import SwiftUI
class CounterService: ObservableObject {
@Published var counterTime: Int = 0
func startCounting(){
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
if UserDefaults.standard.string(forKey: "timeInBg") != nil {
self.counterTime = Int(UserDefaults.standard.double(forKey: "timeInBg")) + self.counterTime
UserDefaults.standard.removeObject(forKey: "timeInBg")
}
self.counterTime += 1
}
}
}
SceneDelegate
File:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let userDefaults = UserDefaults.standard
var appIsDeadAt: Double?
var appIsBackAliveAt: Double?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let contentView = ContentView().environment(\.managedObjectContext, context)
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
}
func sceneDidBecomeActive(_ scene: UIScene) {
}
func sceneWillResignActive(_ scene: UIScene) {
}
func sceneWillEnterForeground(_ scene: UIScene) {
appIsBackAliveAt = Date().timeIntervalSince1970
if appIsDeadAt != nil && appIsBackAliveAt != nil {
let finalTime = (appIsBackAliveAt! - appIsDeadAt!)
userDefaults.setValue(finalTime.rounded(), forKey: "timeInBg")
}
}
func sceneDidEnterBackground(_ scene: UIScene) {
appIsDeadAt = Date().timeIntervalSince1970
(UIApplication.shared.delegate as? AppDelegate)?.saveContext()
}
}
Voila! here you go the result:
I hope this answer your question.