2

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])
        }
        
        
    }

1 Answers1

3

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?

  1. Let's go to SceneDelegate and declare those variables

    var appIsDeadAt: Double?
    var appIsBackAliveAt: Double?
    
  2. We need to save the time when the user enter the background in sceneDidEnterBackground

    appIsDeadAt = Date().timeIntervalSince1970
    
  3. 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
    
  4. Now we need to calculate the total time that the application stayed in the background

    let finalTime = (appIsBackAliveAt! - appIsDeadAt!)
    
  5. 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:

Result

I hope this answer your question.

Cod3rMax
  • 878
  • 3
  • 18
  • @ЕрмолайАбузув you are welcome, make sure to pass by gain later, i'll update the answer as well to put an example from my own app that is released in applestore :) – Cod3rMax Dec 17 '20 at 15:14
  • sounds good:) I hope soon i within touch to share with you my app on appstore with timer with your fix :) – Ермолай Абузув Dec 17 '20 at 15:53
  • @ЕрмолайАбузув i did update it, and to make this problem more clear since i have seen a lot of problems about this, i'll create an example project and put it on github to see how it works exactly and share it later :) have a great day – Cod3rMax Dec 17 '20 at 15:56
  • Thank you very much for helping to explain this! This is one of the best explanations I've seen, thanks friend! The only thing is that I have a question. Please tell me - based on the 3 points of your answer - the user must turn on the application to calculate the time through addition. It turns out that if he (user) does not turn on the application and will just wait for the timer to end, the program will not be able to calculate and therefore end the timer? – Ермолай Абузув Dec 18 '20 at 07:45
  • no it's fine don't worry, you need to use if statment only, lets say you want to stop the timer on 60 seconds, calculate the finalTime + old time, if the calculation is 60 or more then stop the timer if not let the timer continue. – Cod3rMax Dec 18 '20 at 14:42
  • thanks a lot & i'm a bit confusing. Will this method be work if user will not open app again? then it turns out that the variable that is responsible for recording the time that has passed since the application was closed will be empty? – Ермолай Абузув Dec 18 '20 at 15:19
  • yes it will work without problems. when the application will be totaly closed the timer will be stopped – Cod3rMax Dec 18 '20 at 16:03
  • ok friend thanks a lot! So maybe you can help me please with other issue((( https://stackoverflow.com/questions/65362695/add-check-for-user-if-he-try-to-link-to-an-already-linked-account-in-facebook-et – Ермолай Абузув Dec 18 '20 at 19:17