问题描述
我正在尝试向我的 SwiftUI 应用程序添加超时功能。达到超时时应更新视图。我在另一个 thread 上找到了代码,它适用于超时部分,但我无法更新视图。
我在 UIApplication 扩展中使用静态属性来切换超时标志。当此静态属性更改时,似乎不会通知视图。这样做的正确方法是什么?
补充说明:
@workingdog 在下面提出了一个答案。这并不完全有效,因为在实际的应用程序中,不仅有一个视图,而且用户可以在多个视图之间导航。所以,我正在寻找一个全局计时器,无论当前视图是什么,它都可以通过任何触摸操作重置。
在示例代码中,全局计时器工作,但是当静态变量 UIApplication.timeout 更改为 true 时,视图不会注意到。
我怎样才能更新视图?有什么比静态变量更适合这个目的吗?或者计时器不应该在 UIApplication 扩展中开始?
这是我的代码:
import SwiftUI
@main
struct TimeoutApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear(perform: UIApplication.shared.addTapGestureRecognizer)
}
}
}
extension UIApplication {
private static var timerToDetectInactivity: Timer?
static var timeout = false
func addTapGestureRecognizer() {
guard let window = windows.first else { return }
let tapGesture = UITapGestureRecognizer(target: self,action: #selector(tapped))
tapGesture.requiresExclusiveTouchType = false
tapGesture.cancelsTouchesInView = false
tapGesture.delegate = self
window.addGestureRecognizer(tapGesture)
}
private func resetTimer() {
let showScreenSaverInSeconds: TimeInterval = 5
if let timerToDetectInactivity = UIApplication.timerToDetectInactivity {
timerToDetectInactivity.invalidate()
}
UIApplication.timerToDetectInactivity = Timer.scheduledTimer(
timeInterval: showScreenSaverInSeconds,target: self,selector: #selector(timeout),userInfo: nil,repeats: false
)
}
@objc func timeout() {
print("Timeout")
Self.timeout = true
}
@objc func tapped(_ sender: UITapGestureRecognizer) {
if !Self.timeout {
print("Tapped")
self.resetTimer()
}
}
}
extension UIApplication: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text(UIApplication.timeout ? "TimeOut" : "Hello World!")
.padding()
Button("Print to Console") {
print(UIApplication.timeout ? "Timeout reached,why is view not updated?" : "Hello World!")
}
}
}
}
解决方法
你可以这样做 - >
-
创建“UIApplication”的子类(使用单独的文件,如 MYApplication.swift)。
-
在文件中使用以下代码。
-
现在一旦用户停止,方法“idle_timer_exceeded”将被调用 触摸屏幕。
-
使用通知更新 UI
import UIKit
import Foundation
private let g_secs = 5.0 // Set desired time
class MYApplication: UIApplication
{
var idle_timer : dispatch_cancelable_closure?
override init()
{
super.init()
reset_idle_timer()
}
override func sendEvent( event: UIEvent )
{
super.sendEvent( event )
if let all_touches = event.allTouches() {
if ( all_touches.count > 0 ) {
let phase = (all_touches.anyObject() as UITouch).phase
if phase == UITouchPhase.Began {
reset_idle_timer()
}
}
}
}
private func reset_idle_timer()
{
cancel_delay( idle_timer )
idle_timer = delay( g_secs ) { self.idle_timer_exceeded() }
}
func idle_timer_exceeded()
{
// You can broadcast notification here and use an observer to trigger the UI update function
reset_idle_timer()
}
}
您可以查看此帖子的来源 here。
,要在达到超时时更新视图,您可以执行以下操作:
import SwiftUI
@main
struct TimeoutApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@State var thingToUpdate = "tap the screen to rest the timer"
@State var timeRemaining = 5
let timer = Timer.publish(every: 1,on: .main,in: .common).autoconnect()
var body: some View {
VStack (spacing: 30) {
Text("\(thingToUpdate)")
Text("\(timeRemaining)")
.onReceive(timer) { _ in
if timeRemaining > 0 {
timeRemaining -= 1
} else {
thingToUpdate = "refreshed,tap the screen to rest the timer"
}
}
}
.frame(minWidth: 0,idealWidth: .infinity,maxWidth: .infinity,minHeight: 0,idealHeight: .infinity,maxHeight: .infinity,alignment: .center)
.contentShape(Rectangle())
.onTapGesture {
thingToUpdate = "tap the screen to rest the timer"
timeRemaining = 5
}
}
}
,
这是我使用“环境”的“全局”计时器代码。虽然它对我有用,但似乎很多 工作只是为了做一些简单的事情。这让我相信一定有 一个更好的方法来做到这一点。不管怎样,也许你可以在这里回收一些想法。
import SwiftUI
@main
struct TestApp: App {
@StateObject var globalTimer = MyGlobalTimer()
var body: some Scene {
WindowGroup {
ContentView().environment(\.globalTimerKey,globalTimer)
}
}
}
struct MyGlobalTimerKey: EnvironmentKey {
static let defaultValue = MyGlobalTimer()
}
extension EnvironmentValues {
var globalTimerKey: MyGlobalTimer {
get { return self[MyGlobalTimerKey] }
set { self[MyGlobalTimerKey] = newValue }
}
}
class MyGlobalTimer: ObservableObject {
@Published var timeRemaining: Int = 5
var timer = Timer.publish(every: 1,in: .common).autoconnect()
func reset(_ newValue: Int = 5) {
timeRemaining = newValue
}
}
struct ContentView: View {
@Environment(\.globalTimerKey) var globalTimer
@State var refresh = true
var body: some View {
GlobalTimerView { // <-- this puts the content into a tapable view
// the content
VStack {
Text(String(globalTimer.timeRemaining))
.accentColor(refresh ? .black :.black) // <-- trick to refresh the view
.onReceive(globalTimer.timer) { _ in
if globalTimer.timeRemaining > 0 {
globalTimer.timeRemaining -= 1
refresh.toggle() // <-- trick to refresh the view
print("----> time remaining: \(globalTimer.timeRemaining)")
} else {
// do something at every time step after the countdown
print("----> do something ")
}
}
}
}
}
}
struct GlobalTimerView<Content: View>: View {
@Environment(\.globalTimerKey) var globalTimer
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
ZStack {
content
Color(white: 1.0,opacity: 0.001)
.frame(minWidth: 0,alignment: .center)
.contentShape(Rectangle())
.onTapGesture {
globalTimer.reset()
}
}.onAppear {
globalTimer.reset()
}
}
}