Most solutions I see detect user activity by overriding UIApplication.sendEvent
(Example). We can't do that easily in SwiftUI, but it is possible if you do method swizzling. That is quite hacky though.
You can swizzle the methods just before your app launches by writing your own entry point.
@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()
}
}
}
The swizzled method is:
class ActivityDetector: NSObject {
private typealias SendEventFunc = @convention(c) (AnyObject, Selector, UIEvent) -> Void
@objc func mySendEvent(_ event: UIEvent) {
// call the original sendEvent
// from: https://mcmap.net/q/975487/-calling-original-function-from-swizzled-function
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)
}
}
Now in your view, you can listen for the notification:
@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") {}
}
}
Using Timer.publish
here is kind of wasteful, since you only need one output from the publisher. Consider using a Task
for example:
@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
}
}
.onScreenActivation()
modifier. Maybe it is still ahead of Apple to develop. – Astounding