当用户隐藏应用程序或关闭设备时,swiftui 中的自定义计时器停止在后台获取中计数

Custom timer in swiftui stops to count in background fetch when user hide app or turn off device

我正在尝试使用 this cool timer from kavsoft

但是当我隐藏应用程序或关闭屏幕设备(不是关闭应用程序)时,此计时器在后台模式下几秒或几分钟后停止计数并停止。如果我打开应用程序,它会再次继续倒计时。

我尝试了原始代码和修改后的代码。在我的中,我将时分秒的格式设置为。在其中任何一个中,后台模式下的计数都会停止工作。 有什么办法可以解决吗? 我在应用程序中的需求可能需要长达 2 小时才能在后台模式下工作。 这是我在 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])
        }
        
        
    }

我以前遇到过这个问题,我一直在努力解决它,但是我没有得到任何有用的答案。我尝试激活 Background Modes 但没有成功。

下面是我是如何克服这个问题的:

假设你的计数器已经开始计数了,现在计数器在5 seconds,然后用户突然进入后台我们需要做什么?

  1. 我们去 SceneDelegate 声明那些变量

    var appIsDeadAt: Double?
    var appIsBackAliveAt: Double?
    
  2. 我们需要在sceneDidEnterBackground

    中节省用户进入后台的时间
    appIsDeadAt = Date().timeIntervalSince1970
    
  3. 当用户再次进入应用程序时,我们需要计算应用程序在后台停留的时间。转到 sceneWillEnterForeground 并获取应用程序重新激活的时间

    appIsBackAliveAt = Date().timeIntervalSince1970
    
  4. 现在我们需要计算应用程序在后台停留的总时间

    let finalTime = (appIsBackAliveAt! - appIsDeadAt!)
    
  5. 最后,假设应用程序在后台停留了 10 seconds,之前的时间是 5 5 + finalTime(即 10 秒)总时间为 15 seconds,然后更新您的计数器时间以从 15 seconds.

    继续计数

注意:使用 UserDefaults 将值传递给您的计数器,以方便您。

一个实时示例来自我自己在 applestore 上发布的应用程序:Chatiw

我的计时器基本上是,当用户观看广告视频时,我会奖励他 300 seconds 没有广告。

完成上述所有步骤后,当我有最后的时间时,我会把它存储在UserDefaults:

userDefaults.setValue(finalTime.rounded(), forKey: "timeInBg")

然后在包含计数器的主代码中,我会将计数器更新为新时间:

adsRemovalCounter = adsRemovalCounter - Int(self.userDefaults.double(forKey: "timeInBg"))

之后我会把finalTimeUserDefaults键删掉,这样就不会在用户再次进入后台时,影响到下一步的计算了。

self.userDefaults.removeObject(forKey: "timeInBg")

这是我刚刚在SwiftUI中制作的示例:

ContentView 文件:

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 文件:

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 文件:

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


}

瞧!给你结果:

我希望这能回答你的问题。