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:

preview

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!")
  }
}

Next Post Previous Post