当用户隐藏应用程序或关闭设备时,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
,然后用户突然进入后台我们需要做什么?
我们去 SceneDelegate
声明那些变量
var appIsDeadAt: Double?
var appIsBackAliveAt: Double?
我们需要在sceneDidEnterBackground
中节省用户进入后台的时间
appIsDeadAt = Date().timeIntervalSince1970
当用户再次进入应用程序时,我们需要计算应用程序在后台停留的时间。转到 sceneWillEnterForeground
并获取应用程序重新激活的时间
appIsBackAliveAt = Date().timeIntervalSince1970
现在我们需要计算应用程序在后台停留的总时间
let finalTime = (appIsBackAliveAt! - appIsDeadAt!)
最后,假设应用程序在后台停留了 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"))
之后我会把finalTime
的UserDefaults
键删掉,这样就不会在用户再次进入后台时,影响到下一步的计算了。
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()
}
}
瞧!给你结果:
我希望这能回答你的问题。
我正在尝试使用 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
,然后用户突然进入后台我们需要做什么?
我们去
SceneDelegate
声明那些变量var appIsDeadAt: Double? var appIsBackAliveAt: Double?
我们需要在
中节省用户进入后台的时间sceneDidEnterBackground
appIsDeadAt = Date().timeIntervalSince1970
当用户再次进入应用程序时,我们需要计算应用程序在后台停留的时间。转到
sceneWillEnterForeground
并获取应用程序重新激活的时间appIsBackAliveAt = Date().timeIntervalSince1970
现在我们需要计算应用程序在后台停留的总时间
let finalTime = (appIsBackAliveAt! - appIsDeadAt!)
最后,假设应用程序在后台停留了
继续计数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"))
之后我会把finalTime
的UserDefaults
键删掉,这样就不会在用户再次进入后台时,影响到下一步的计算了。
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()
}
}
瞧!给你结果:
我希望这能回答你的问题。