提问人:Voyteck 提问时间:9/30/2023 最后编辑:Voyteck 更新时间:9/30/2023 访问量:76
SwiftUI 不活动计时器
SwiftUI inactivity timer
问:
我想在用户 30 秒未触摸屏幕后显示提示。为此,每当触摸屏幕时都需要调用此函数。restartInactivityTimer()
有没有办法发现任何屏幕触摸来重新启动计时器,但没有“消耗”它的点击手势?我真的需要从每个 onTapGesture
(子)视图闭包进行调用,就像下面的代码片段一样吗?restartInactivityTimer()
struct MyView: View {
@State private var inactivityTimer: Timer?
var body: some View {
VStack {
subView1
subView2
subView3
subView4
}
.onAppear {
restartInactivityTimer()
}
.onTapGesture {
// not reaching here when any subView is touched
restartInactivityTimer()
}
}
func restartInactivityTimer() {
inactivityTimer?.invalidate()
inactivityTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: false) { _ in
showHint()
}
}
var subView1: some View {
// ...
.onTapGesture {
// ... other actions for tapping that subview
restartInactivityTimer()
}
}
// ... other subviews
}
答:
1赞
Sweeper
9/30/2023
#1
我看到的大多数解决方案都通过覆盖来检测用户活动(示例)。在 SwiftUI 中,我们不能轻易做到这一点,但如果你做方法 swizzling,这是可能的。不过,这很棘手。UIApplication.sendEvent
您可以在应用启动之前通过编写自己的入口点来切换这些方法。
@main
struct EntryPoint {
// swizzle here
static func main() {
let original = class_getInstanceMethod(UIApplication.self, #selector(UIApplication.sendEvent))!
let new = class_getInstanceMethod(ActivityDetector.self, #selector(ActivityDetector.mySendEvent))!
method_exchangeImplementations(original, new)
// launch your app
YourApp.main()
}
}
struct YourApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
swizzled 方法是:
class ActivityDetector: NSObject {
private typealias SendEventFunc = @convention(c) (AnyObject, Selector, UIEvent) -> Void
@objc func mySendEvent(_ event: UIEvent) {
// call the original sendEvent
// from: https://stackoverflow.com/a/61523711/5133585
unsafeBitCast(
class_getMethodImplementation(ActivityDetector.self, #selector(mySendEvent)),
to: SendEventFunc.self
)(self, #selector(UIApplication.sendEvent), event)
// send a notification, just like in the non-SwiftUI solutions
NotificationCenter.default.post(name: .init("UserActivity"), object: nil)
}
}
现在,在您的视图中,您可以收听通知:
@State var presentAlert = false
@State var timer = Timer.publish(every: 60, on: .current, in: .common).autoconnect()
var body: some View {
Text("Foo")
// time is up!
.onReceive(timer) { _ in
presentAlert = true
timer.upstream.connect().cancel()
}
// user did something!
.onReceive(NotificationCenter.default.publisher(for: .init("UserActivity")), perform: { _ in
timer.upstream.connect().cancel()
timer = Timer.publish (every: 5, on: .current, in: .common).autoconnect()
})
.alert("Foo", isPresented: $presentAlert) {
Button("OK") {}
}
}
使用 here 有点浪费,因为你只需要发布者的一个输出。例如,请考虑使用 a:Timer.publish
Task
@State var presentAlert = false
@State var task: Task<Void, Error>?
var body: some View {
Text("Foo")
.onAppear {
resetTask()
}
.onReceive(NotificationCenter.default.publisher(for: .init("UserActivity")), perform: { _ in
task?.cancel()
resetTask()
})
.alert("Foo", isPresented: $presentAlert) {
Button("OK") {}
}
}
@MainActor
func resetTask() {
task = Task {
try await Task.sleep(for: .seconds(60))
try Task.checkCancellation()
presentAlert = true
}
}
评论
0赞
Voyteck
10/2/2023
谢谢@Sweeper的全面回答。所提出的解决方案比原始解决方案更棘手和令人厌烦。我希望有一个聪明的东西,比如修饰符。也许它仍然领先于苹果的发展。.onScreenActivation()
0赞
Sweeper
10/2/2023
@Voyteck 甚至 UIKit 解决方案也没有“聪明的东西”。此外,“swizzling”在这里不是形容词。它只是意味着交换两个方法的实现。.onScreenActiviation
0赞
Martin Claesson
12/22/2023
这应该是公认的答案。使用 .onTapGesture 不会捕获所有用户输入。
评论