diff --git a/Sources/Amplitude/Constants.swift b/Sources/Amplitude/Constants.swift index 750bb2c..e8d9ca4 100644 --- a/Sources/Amplitude/Constants.swift +++ b/Sources/Amplitude/Constants.swift @@ -101,6 +101,7 @@ public struct Constants { static let AMP_APP_TARGET_VIEW_CLASS = "\(AMP_AMPLITUDE_PREFIX)Target View Class" static let AMP_APP_TARGET_TEXT = "\(AMP_AMPLITUDE_PREFIX)Target Text" static let AMP_APP_HIERARCHY = "\(AMP_AMPLITUDE_PREFIX)Hierarchy" + static let AMP_APP_GESTURE_RECOGNIZER = "\(AMP_AMPLITUDE_PREFIX)Gesture Recognizer" public struct Configuration { public static let FLUSH_QUEUE_SIZE = 30 diff --git a/Sources/Amplitude/Events/UserInteractionEvent.swift b/Sources/Amplitude/Events/UserInteractionEvent.swift index 17dea19..b3579d3 100644 --- a/Sources/Amplitude/Events/UserInteractionEvent.swift +++ b/Sources/Amplitude/Events/UserInteractionEvent.swift @@ -8,7 +8,8 @@ public class UserInteractionEvent: BaseEvent { action: String, targetViewClass: String, targetText: String? = nil, - hierarchy: String + hierarchy: String, + gestureRecognizer: String? = nil ) { self.init(eventType: Constants.AMP_USER_INTERACTION_EVENT, eventProperties: [ Constants.AMP_APP_VIEW_CONTROLLER: viewController, @@ -17,7 +18,8 @@ public class UserInteractionEvent: BaseEvent { Constants.AMP_APP_ACTION: action, Constants.AMP_APP_TARGET_VIEW_CLASS: targetViewClass, Constants.AMP_APP_TARGET_TEXT: targetText, - Constants.AMP_APP_HIERARCHY: hierarchy + Constants.AMP_APP_HIERARCHY: hierarchy, + Constants.AMP_APP_GESTURE_RECOGNIZER: gestureRecognizer ]) } } diff --git a/Sources/Amplitude/Plugins/iOS/UIKitUserInteractions.swift b/Sources/Amplitude/Plugins/iOS/UIKitUserInteractions.swift index 193f4c1..b767386 100644 --- a/Sources/Amplitude/Plugins/iOS/UIKitUserInteractions.swift +++ b/Sources/Amplitude/Plugins/iOS/UIKitUserInteractions.swift @@ -6,42 +6,23 @@ class UIKitUserInteractions { private static let lock = NSLock() - private static let addNotificationObservers: () = { + private static let addNotificationObservers: Void = { NotificationCenter.default.addObserver(UIKitUserInteractions.self, selector: #selector(didBeginEditing), name: UITextField.textDidBeginEditingNotification, object: nil) NotificationCenter.default.addObserver(UIKitUserInteractions.self, selector: #selector(didEndEditing), name: UITextField.textDidEndEditingNotification, object: nil) NotificationCenter.default.addObserver(UIKitUserInteractions.self, selector: #selector(didBeginEditing), name: UITextView.textDidBeginEditingNotification, object: nil) NotificationCenter.default.addObserver(UIKitUserInteractions.self, selector: #selector(didEndEditing), name: UITextView.textDidEndEditingNotification, object: nil) }() - private static let swizzleSendAction: () = { - let applicationClass = UIApplication.self - - let originalSelector = #selector(UIApplication.sendAction) - let swizzledSelector = #selector(UIApplication.amp_sendAction) - - guard - let originalMethod = class_getInstanceMethod(applicationClass, originalSelector), - let swizzledMethod = class_getInstanceMethod(applicationClass, swizzledSelector) - else { return } - - let originalImp = method_getImplementation(originalMethod) - let swizzledImp = method_getImplementation(swizzledMethod) - - class_replaceMethod(applicationClass, - swizzledSelector, - originalImp, - method_getTypeEncoding(originalMethod)) - class_replaceMethod(applicationClass, - originalSelector, - swizzledImp, - method_getTypeEncoding(swizzledMethod)) + private static let setupMethodSwizzling: Void = { + swizzleMethod(UIApplication.self, from: #selector(UIApplication.sendAction), to: #selector(UIApplication.amp_sendAction)) + swizzleMethod(UIGestureRecognizer.self, from: #selector(setter: UIGestureRecognizer.state), to: #selector(UIGestureRecognizer.amp_setState)) }() static func register(_ amplitude: Amplitude) { lock.withLock { amplitudeInstances.add(amplitude) } - swizzleSendAction + setupMethodSwizzling addNotificationObservers } @@ -60,6 +41,25 @@ class UIKitUserInteractions { $0.track(event: userInteractionEvent) } } + + private static func swizzleMethod(_ cls: AnyClass?, from original: Selector, to swizzled: Selector) { + guard + let originalMethod = class_getInstanceMethod(cls, original), + let swizzledMethod = class_getInstanceMethod(cls, swizzled) + else { return } + + let originalImp = method_getImplementation(originalMethod) + let swizzledImp = method_getImplementation(swizzledMethod) + + class_replaceMethod(cls, + swizzled, + originalImp, + method_getTypeEncoding(originalMethod)) + class_replaceMethod(cls, + original, + swizzledImp, + method_getTypeEncoding(swizzledMethod)) + } } extension UIApplication { @@ -84,6 +84,46 @@ extension UIApplication { } } +extension UIGestureRecognizer { + @objc func amp_setState(_ state: UIGestureRecognizer.State) { + amp_setState(state) + + guard state == .ended, let view else { return } + + let gestureType = switch self { + case is UITapGestureRecognizer: "Tap" + case is UISwipeGestureRecognizer: "Swipe" + case is UIPanGestureRecognizer: "Pan" + case is UILongPressGestureRecognizer: "Long Press" +#if !os(tvOS) + case is UIPinchGestureRecognizer: "Pinch" + case is UIRotationGestureRecognizer: "Rotation" + case is UIScreenEdgePanGestureRecognizer: "Screen Edge Pan" +#endif + default: "Custom Gesture" + } + + let userInteractionEvent = eventFromData(with: gestureType, from: view) + + UIKitUserInteractions.amplitudeInstances.allObjects.forEach { + $0.track(event: userInteractionEvent) + } + } + + func eventFromData(with action: String, from view: UIView) -> UserInteractionEvent { + let viewData = view.extractData(with: action) + return UserInteractionEvent( + viewController: viewData.viewController, + title: viewData.title, + accessibilityLabel: viewData.accessibilityLabel, + action: viewData.action, + targetViewClass: viewData.targetViewClass, + targetText: viewData.targetText, + hierarchy: viewData.hierarchy, + gestureRecognizer: descriptiveTypeName) + } +} + extension UIView { private static let viewHierarchyDelimiter = " -> " @@ -125,15 +165,17 @@ extension UIView { } extension UIResponder { - var descriptiveTypeName: String { - String(describing: type(of: self)) - } - var owningViewController: UIViewController? { return self as? UIViewController ?? next?.owningViewController } } +extension NSObject { + var descriptiveTypeName: String { + String(describing: type(of: self)) + } +} + protocol ActionTrackable { var amp_title: String? { get } func amp_shouldTrack(_ action: Selector, for event: UIEvent?) -> Bool