Reading time: 2 min
This recipe shows how to detect user interactions - taps and touches anywhere on the screen - in SwiftUI. The method doesn't interfere with touch events on UI components and can be turned on/off on demand.
The end result looks like this:
The recipe
The recipe involves registering a UITapGestureRecognizer
for the current window. It will be configured to still pass any touches to underlying UI components. On tap, we'll send a Notification
that the rest of the app can then use.
Here's the full code:
extension Notification.Name {
// custom notification when user activity is detected
static let userActivityDetected = Notification.Name("UserActivityDetected")
}
extension UIApplication {
// names the gesture recognizer so that it can be removed later on
static let userActivityGestureRecognizer = "userActivityGestureRecognizer"
// Returns `true` if user activity tracker is registered on the app.
var hasUserActivityTracker: Bool {
windows.first?.gestureRecognizers
?.contains(where: { $0.name == UIApplication.userActivityGestureRecognizer }) == true
}
// Adds a tap gesture recognizer to intercept any touches, while still
// propagating interactions to UI elements.
func addUserActivityTracker() {
guard let window = windows.first
else {
return
}
let gesture = UITapGestureRecognizer(target: window, action: nil)
gesture.requiresExclusiveTouchType = false
gesture.cancelsTouchesInView = false
gesture.delegate = self
gesture.name = UIApplication.userActivityGestureRecognizer
window.addGestureRecognizer(gesture)
}
// Removes the tap gesture recognizer that detects user interactions.
func removeUserActivityTracker() {
guard let window = windows.first,
let gesture = window.gestureRecognizers?.first(where: { $0.name == UIApplication.userActivityGestureRecognizer })
else {
return
}
window.removeGestureRecognizer(gesture)
}
}
extension UIApplication: UIGestureRecognizerDelegate {
// Send a notification whenever a touch is detected.
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch) -> Bool {
NotificationCenter.default.post(name: .userActivityDetected, object: nil)
return true
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
true
}
}
Testing it out
Here's a little set-up to test everything in action. The view has a number of UI elements that the user can still interact with even when top-level UITapGestureRecognizer
is on. Each userActivityDetected
notification is captured in the view model and prints a message to the log:
struct UserActivityTest: View {
@ObservedObject private(set) var viewModel: UserActivityTestViewModel
@State private var date = Date()
var body: some View {
TabView {
VStack {
DatePicker("Date Picker", selection: $date)
.datePickerStyle(.graphical)
Button("A button") {
print("I'm tapped!")
}
Button("Toggle tracker") {
if UIApplication.shared.hasUserActivityTracker {
UIApplication.shared.removeUserActivityTracker()
} else {
UIApplication.shared.addUserActivityTracker()
}
}
}
.tabItem {
Text("Left")
}
List {
Text("Top")
Text("Bottom")
}
.tabItem {
Text("Right")
}
}
.onAppear(perform: UIApplication.shared.addUserActivityTracker)
}
}
class UserActivityTestViewModel: ObservableObject {
init() {
NotificationCenter.default.addObserver(self,
selector: #selector(handleUserInteraction(_:)),
name: .userActivityDetected,
object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
@objc private func handleUserInteraction(_ notification: Notification) {
print("User interaction!")
}
}