【问题标题】:How to get a view to update automatically after period of inactivity?如何在不活动期后自动更新视图?
【发布时间】:2021-06-13 09:30:46
【问题描述】:

我正在尝试向我的 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!")
      }
    }
  }
}

【问题讨论】:

    标签: ios swift swiftui uiapplication


    【解决方案1】:

    要在超时时更新视图,您可以执行以下操作:

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

    【讨论】:

    • 谢谢。我试过了,但它只完成了一半的工作。计时器需要在任何屏幕触摸时重置,而不仅仅是当我按下专用的重置按钮时。如果您运行我的示例代码并查看控制台输出,您将看到它的作用。
    • 好的,我已经更新了代码,你可以点击屏幕来更新视图。
    • 谢谢你,@workingdog。这行得通,...几乎。我仍然遇到的问题是您提出的解决方案,计时器是附加到视图的修饰符。当我添加第二个视图时,我需要第二个计时器。当我在视图之间导航时,我最终会得到多个计时器。我真正想要的是一个全局计时器,无论当前视图如何,它都会被重置。我将更改问题以使其清楚。无论如何,谢谢。
    • 您可以尝试使用 ObservableObject 来实现您想要实现的目标。例如: "class MyGlobalTimer: ObservableObject { @Published var ... }" 。您可以使用“@ObservedObject var myGlobalTimer = MyGlobalTimer()”轻松将此 ObservableObject 传递给所有视图或使用“@EnvironmentObject”
    • 是的,如果全局计时器在一个类中,那么我可以将超时标志放在已发布的变量中。我曾尝试将 UIApplication 子类化,但后来我无法让应用程序使用子类而不是 UIApplication。能否提供一个代码示例如何实现?
    【解决方案2】:

    你可以这样做——>

    1. 创建“UIApplication”的子类(使用单独的文件,如 MYApplication.swift)。

    2. 在文件中使用以下代码。

    3. 现在,一旦用户停止,方法“idle_timer_exceeded”将被调用 触摸屏幕。

    4. 使用通知更新 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

    【讨论】:

    • 谢谢。我之前看到过类似的实现,并尝试过。但是覆盖 sendEvent 在 SwiftUI 中不起作用,或者是吗?
    【解决方案3】:

    这是我使用“环境”的“全局”计时器的代码。虽然它对我有用,但似乎很多 工作只是为了做一些简单的事情。这让我相信一定有 一个更好的方法来做到这一点。无论如何,也许你可以在这里回收一些想法。

    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, on: .main, 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, idealWidth: .infinity, maxWidth: .infinity,
                           minHeight: 0, idealHeight: .infinity, maxHeight: .infinity,
                           alignment: .center)
                    .contentShape(Rectangle())
                    .onTapGesture {
                        globalTimer.reset()
                    }
            }.onAppear {
                globalTimer.reset()
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-10
      • 2023-03-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多