Skip to content

Commit 8213e95

Browse files
Merge pull request #2717 from DataDog/bplasovska/RUM-14763/scroll-swipe-tracking
RUM-14763 Add OOB scroll and swipe action tracking Co-authored-by: barboraplasovska <barbora.plasovska@datadoghq.com>
2 parents 75bc208 + 95bda61 commit 8213e95

13 files changed

Lines changed: 919 additions & 1 deletion

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
22

3+
- [FEATURE] Add OOB scroll and swipe action tracking. See [#2717][]
34
- [IMPROVEMENT] Rename `DDRUMErrorEventErrorMeta` to `DDRUMErrorEventErrorMetaInfo`, add support of custom Objective-C runtime names for generated RUM models. See [#2705][]
45
- [FEATURE] Add `trackResourceHeaders` configuration to capture HTTP request and response headers in RUM Resource events. See [#2721][]
56

@@ -1062,6 +1063,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO
10621063
[#2688]: https://github.com/DataDog/dd-sdk-ios/pull/2688
10631064
[#2705]: https://github.com/DataDog/dd-sdk-ios/pull/2705
10641065
[#2721]: https://github.com/DataDog/dd-sdk-ios/pull/2721
1066+
[#2717]: https://github.com/DataDog/dd-sdk-ios/pull/2717
10651067

10661068
[@00fa9a]: https://github.com/00FA9A
10671069
[@britton-earnin]: https://github.com/Britton-Earnin

Datadog/Datadog.xcodeproj/project.pbxproj

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,14 @@
12451245
A7F651302B7655DE004B0EDB /* UIImageResourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F6512F2B7655DE004B0EDB /* UIImageResourceTests.swift */; };
12461246
A7FA98CE2BA1A6930018D6B5 /* MethodCalledMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */; };
12471247
A7FA98CF2BA1A6930018D6B5 /* MethodCalledMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */; };
1248+
AA0001012A000001000B0001 /* RUMScrollHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000001000A0001 /* RUMScrollHandler.swift */; };
1249+
AA0001012A000001000C0001 /* RUMScrollHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000001000A0001 /* RUMScrollHandler.swift */; };
1250+
AA0001012A000002000B0001 /* UIScrollViewDelegateProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000002000A0001 /* UIScrollViewDelegateProxy.swift */; };
1251+
AA0001012A000002000C0001 /* UIScrollViewDelegateProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000002000A0001 /* UIScrollViewDelegateProxy.swift */; };
1252+
AA0001012A000003000B0001 /* UIScrollViewSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000003000A0001 /* UIScrollViewSwizzler.swift */; };
1253+
AA0001012A000003000C0001 /* UIScrollViewSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000003000A0001 /* UIScrollViewSwizzler.swift */; };
1254+
AA0001012A000005000B0001 /* UIScrollViewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000005000A0001 /* UIScrollViewHandler.swift */; };
1255+
AA0001012A000005000C0001 /* UIScrollViewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000005000A0001 /* UIScrollViewHandler.swift */; };
12481256
AF9357FB8B8BF79EDD56F7C0 /* FlagsEvaluationFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD117A3F5E4EFA0335A8268F /* FlagsEvaluationFeature.swift */; };
12491257
B3E46CAB2D91B3AD00BABF66 /* NetworkContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E46CAA2D91B3A400BABF66 /* NetworkContextProvider.swift */; };
12501258
B3E46CAC2D91B3AD00BABF66 /* NetworkContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E46CAA2D91B3A400BABF66 /* NetworkContextProvider.swift */; };
@@ -1540,6 +1548,9 @@
15401548
D23F8EB329DDCD38001CFAE8 /* ErrorMessageReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21C26ED28AFB65B005DD405 /* ErrorMessageReceiverTests.swift */; };
15411549
D23F8EB429DDCD38001CFAE8 /* RUMApplicationScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617B953F24BF4DB300E6F443 /* RUMApplicationScopeTests.swift */; };
15421550
D23F8EB629DDCD38001CFAE8 /* RUMViewsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29889C72734136200A4D1A9 /* RUMViewsHandlerTests.swift */; };
1551+
AA0001012A000004000B0001 /* RUMScrollHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000004000A0001 /* RUMScrollHandlerTests.swift */; };
1552+
AA0001012A000006000B0001 /* UIScrollViewDelegateProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000006000A0001 /* UIScrollViewDelegateProxyTests.swift */; };
1553+
AA0001012A000007000B0001 /* UIScrollViewSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000007000A0001 /* UIScrollViewSwizzlerTests.swift */; };
15431554
D23F8EB829DDCD38001CFAE8 /* RUMActionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615C3195251DD5080018781C /* RUMActionsHandlerTests.swift */; };
15441555
D23F8EBA29DDCD38001CFAE8 /* ViewIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61C1510C25AC8C1B00362D4B /* ViewIdentifierTests.swift */; };
15451556
D23F8EBE29DDCD38001CFAE8 /* WebViewEventReceiverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E53889B2773C4B300A7DC42 /* WebViewEventReceiverTests.swift */; };
@@ -1760,6 +1771,9 @@
17601771
D29A9FA729DDB483005C54A4 /* RUMCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618715F624DC0CDE00FC0F69 /* RUMCommandTests.swift */; };
17611772
D29A9FAA29DDB483005C54A4 /* RUMViewsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29889C72734136200A4D1A9 /* RUMViewsHandlerTests.swift */; };
17621773
D29A9FAB29DDB483005C54A4 /* RUMUserActionScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617CD0DC24CEDDD300B0B557 /* RUMUserActionScopeTests.swift */; };
1774+
AA0001012A000004000C0001 /* RUMScrollHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000004000A0001 /* RUMScrollHandlerTests.swift */; };
1775+
AA0001012A000006000C0001 /* UIScrollViewDelegateProxyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000006000A0001 /* UIScrollViewDelegateProxyTests.swift */; };
1776+
AA0001012A000007000C0001 /* UIScrollViewSwizzlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012A000007000A0001 /* UIScrollViewSwizzlerTests.swift */; };
17631777
D29A9FAC29DDB483005C54A4 /* RUMActionsHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615C3195251DD5080018781C /* RUMActionsHandlerTests.swift */; };
17641778
D29A9FAE29DDB483005C54A4 /* SessionReplayDependencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615950EA291C029700470E0C /* SessionReplayDependencyTests.swift */; };
17651779
D29A9FB329DDB483005C54A4 /* RUMScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618DCFDE24C75FD300589570 /* RUMScopeTests.swift */; };
@@ -3290,6 +3304,9 @@
32903304
615A4A8824A34FD700233986 /* DDTracerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDTracerTests.swift; sourceTree = "<group>"; };
32913305
615B0F8A2BB33C2800E9ED6C /* AppHangsMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppHangsMonitorTests.swift; sourceTree = "<group>"; };
32923306
615C3195251DD5080018781C /* RUMActionsHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMActionsHandlerTests.swift; sourceTree = "<group>"; };
3307+
AA0001012A000004000A0001 /* RUMScrollHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMScrollHandlerTests.swift; sourceTree = "<group>"; };
3308+
AA0001012A000006000A0001 /* UIScrollViewDelegateProxyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewDelegateProxyTests.swift; sourceTree = "<group>"; };
3309+
AA0001012A000007000A0001 /* UIScrollViewSwizzlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewSwizzlerTests.swift; sourceTree = "<group>"; };
32933310
615D52B72C888C1F00F8B8FC /* SynchronizedAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedAttributes.swift; sourceTree = "<group>"; };
32943311
615D52BA2C88A83A00F8B8FC /* SynchronizedTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedTags.swift; sourceTree = "<group>"; };
32953312
615D52BD2C88A98300F8B8FC /* SynchronizedTagsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedTagsTests.swift; sourceTree = "<group>"; };
@@ -3616,6 +3633,10 @@
36163633
A7F773DB29253F8B00AC1A62 /* B3HTTPHeadersWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = B3HTTPHeadersWriter.swift; sourceTree = "<group>"; };
36173634
A7F773DC29253F8B00AC1A62 /* B3HTTPHeadersReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = B3HTTPHeadersReader.swift; sourceTree = "<group>"; };
36183635
A7FA98CD2BA1A6930018D6B5 /* MethodCalledMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MethodCalledMetric.swift; sourceTree = "<group>"; };
3636+
AA0001012A000001000A0001 /* RUMScrollHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMScrollHandler.swift; sourceTree = "<group>"; };
3637+
AA0001012A000002000A0001 /* UIScrollViewDelegateProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewDelegateProxy.swift; sourceTree = "<group>"; };
3638+
AA0001012A000003000A0001 /* UIScrollViewSwizzler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewSwizzler.swift; sourceTree = "<group>"; };
3639+
AA0001012A000005000A0001 /* UIScrollViewHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollViewHandler.swift; sourceTree = "<group>"; };
36193640
B3BBBCB0265E71C600943419 /* VitalMemoryReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalMemoryReader.swift; sourceTree = "<group>"; };
36203641
B3BBBCBB265E71D100943419 /* VitalMemoryReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VitalMemoryReaderTests.swift; sourceTree = "<group>"; };
36213642
B3E46CAA2D91B3A400BABF66 /* NetworkContextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkContextProvider.swift; sourceTree = "<group>"; };
@@ -5933,15 +5954,27 @@
59335954
children = (
59345955
96F70D572DDE253C00D3736B /* SwiftUIComponentDetectorTests.swift */,
59355956
615C3195251DD5080018781C /* RUMActionsHandlerTests.swift */,
5957+
AA0001012A000004000A0001 /* RUMScrollHandlerTests.swift */,
5958+
AA0001012A000008000A0001 /* UIKit */,
59365959
);
59375960
path = Actions;
59385961
sourceTree = "<group>";
59395962
};
5963+
AA0001012A000008000A0001 /* UIKit */ = {
5964+
isa = PBXGroup;
5965+
children = (
5966+
AA0001012A000006000A0001 /* UIScrollViewDelegateProxyTests.swift */,
5967+
AA0001012A000007000A0001 /* UIScrollViewSwizzlerTests.swift */,
5968+
);
5969+
path = UIKit;
5970+
sourceTree = "<group>";
5971+
};
59405972
6141014D251A578D00E3C2D9 /* Actions */ = {
59415973
isa = PBXGroup;
59425974
children = (
59435975
96F70D442DD793C400D3736B /* RUMAction.swift */,
59445976
61193AAD2CB54C7300C3CDF5 /* RUMActionsHandler.swift */,
5977+
AA0001012A000001000A0001 /* RUMScrollHandler.swift */,
59455978
D29D5A4A273BF81500A687C1 /* UIKit */,
59465979
D29D5A4B273BF82200A687C1 /* SwiftUI */,
59475980
);
@@ -7526,6 +7559,9 @@
75267559
6141015A251A601D00E3C2D9 /* UIEventCommandFactory.swift */,
75277560
6141014E251A57AF00E3C2D9 /* UIApplicationSwizzler.swift */,
75287561
F637AED12697404200516F32 /* UIKitRUMUserActionsPredicate.swift */,
7562+
AA0001012A000002000A0001 /* UIScrollViewDelegateProxy.swift */,
7563+
AA0001012A000005000A0001 /* UIScrollViewHandler.swift */,
7564+
AA0001012A000003000A0001 /* UIScrollViewSwizzler.swift */,
75297565
);
75307566
path = UIKit;
75317567
sourceTree = "<group>";
@@ -10605,6 +10641,10 @@
1060510641
618F2B072D15922400A647C4 /* NextViewActionPredicate.swift in Sources */,
1060610642
6105C50A2CFA222400C4C5EE /* INVMetric.swift in Sources */,
1060710643
D23F8E8E29DDCD28001CFAE8 /* UIEventCommandFactory.swift in Sources */,
10644+
AA0001012A000002000C0001 /* UIScrollViewDelegateProxy.swift in Sources */,
10645+
AA0001012A000001000C0001 /* RUMScrollHandler.swift in Sources */,
10646+
AA0001012A000003000C0001 /* UIScrollViewSwizzler.swift in Sources */,
10647+
AA0001012A000005000C0001 /* UIScrollViewHandler.swift in Sources */,
1060810648
11A2F24C2E70CC08006EDC52 /* FrameInfoProvider.swift in Sources */,
1060910649
11A2F24D2E70CC08006EDC52 /* MediaTimeProvider.swift in Sources */,
1061010650
D23F8E8F29DDCD28001CFAE8 /* RUMUUIDGenerator.swift in Sources */,
@@ -10660,6 +10700,9 @@
1066010700
D23F8EB629DDCD38001CFAE8 /* RUMViewsHandlerTests.swift in Sources */,
1066110701
61C713CB2A3DC22700FA735A /* RUMTests.swift in Sources */,
1066210702
D23F8EB829DDCD38001CFAE8 /* RUMActionsHandlerTests.swift in Sources */,
10703+
AA0001012A000004000B0001 /* RUMScrollHandlerTests.swift in Sources */,
10704+
AA0001012A000006000B0001 /* UIScrollViewDelegateProxyTests.swift in Sources */,
10705+
AA0001012A000007000B0001 /* UIScrollViewSwizzlerTests.swift in Sources */,
1066310706
61C713AE2A3B793E00FA735A /* RUMMonitorProtocolTests.swift in Sources */,
1066410707
6105C4FB2CEBD72600C4C5EE /* TNSMetricTests.swift in Sources */,
1066510708
D23F8EBA29DDCD38001CFAE8 /* ViewIdentifierTests.swift in Sources */,
@@ -11102,6 +11145,10 @@
1110211145
618F2B062D15922400A647C4 /* NextViewActionPredicate.swift in Sources */,
1110311146
6105C5092CFA222400C4C5EE /* INVMetric.swift in Sources */,
1110411147
D29A9F6929DD85BB005C54A4 /* UIEventCommandFactory.swift in Sources */,
11148+
AA0001012A000002000B0001 /* UIScrollViewDelegateProxy.swift in Sources */,
11149+
AA0001012A000001000B0001 /* RUMScrollHandler.swift in Sources */,
11150+
AA0001012A000003000B0001 /* UIScrollViewSwizzler.swift in Sources */,
11151+
AA0001012A000005000B0001 /* UIScrollViewHandler.swift in Sources */,
1110511152
11A2F24A2E70CC08006EDC52 /* FrameInfoProvider.swift in Sources */,
1110611153
11A2F24B2E70CC08006EDC52 /* MediaTimeProvider.swift in Sources */,
1110711154
D29A9F5229DD85BB005C54A4 /* RUMUUIDGenerator.swift in Sources */,
@@ -11156,6 +11203,9 @@
1115611203
D29A9FAA29DDB483005C54A4 /* RUMViewsHandlerTests.swift in Sources */,
1115711204
61C713CA2A3DC22700FA735A /* RUMTests.swift in Sources */,
1115811205
D29A9FAC29DDB483005C54A4 /* RUMActionsHandlerTests.swift in Sources */,
11206+
AA0001012A000004000C0001 /* RUMScrollHandlerTests.swift in Sources */,
11207+
AA0001012A000006000C0001 /* UIScrollViewDelegateProxyTests.swift in Sources */,
11208+
AA0001012A000007000C0001 /* UIScrollViewSwizzlerTests.swift in Sources */,
1115911209
61C713AD2A3B793E00FA735A /* RUMMonitorProtocolTests.swift in Sources */,
1116011210
6105C4FA2CEBD72600C4C5EE /* TNSMetricTests.swift in Sources */,
1116111211
D29A9FB729DDB483005C54A4 /* ViewIdentifierTests.swift in Sources */,
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2019-Present Datadog, Inc.
5+
*/
6+
7+
#if !os(tvOS)
8+
9+
import UIKit
10+
import DatadogInternal
11+
12+
/// Handles scroll and swipe gesture detection on UIScrollView-based components.
13+
/// Tracks scroll lifecycle events, classifies gestures as scroll or swipe based on velocity,
14+
/// and generates RUM commands for the action pipeline.
15+
internal final class RUMScrollHandler: UIScrollViewHandler {
16+
/// Velocity threshold (in points/second) to classify a gesture as a swipe vs. scroll.
17+
/// Gestures with velocity magnitude >= this value are classified as swipe.
18+
static let velocityThreshold: CGFloat = 500
19+
20+
/// Attribute key for gesture direction, matching Android's `action.gesture.direction`.
21+
static let gestureDirectionAttribute: String = "action.gesture.direction"
22+
23+
/// State tracked for an active scroll gesture.
24+
private struct ScrollState {
25+
let startTime: Date
26+
let startOffset: CGPoint
27+
let actionName: String
28+
/// Velocity captured when the user lifts their finger (end of drag).
29+
/// This is the only reliable moment to read velocity — after deceleration it's zero.
30+
var liftVelocity: CGPoint?
31+
}
32+
33+
private let dateProvider: DateProvider
34+
private let predicate: UITouchRUMActionsPredicate
35+
36+
weak var subscriber: RUMCommandSubscriber?
37+
38+
/// Active scrolls keyed by scroll view identity.
39+
private var activeScrolls: [ObjectIdentifier: ScrollState] = [:]
40+
41+
init(
42+
dateProvider: DateProvider,
43+
predicate: UITouchRUMActionsPredicate
44+
) {
45+
self.dateProvider = dateProvider
46+
self.predicate = predicate
47+
}
48+
49+
// MARK: - RUMCommandPublisher
50+
51+
func publish(to subscriber: RUMCommandSubscriber) {
52+
self.subscriber = subscriber
53+
}
54+
55+
// MARK: - UIScrollViewHandler
56+
57+
/// Called when the user begins dragging.
58+
func notify_scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
59+
// Only track user-initiated scrolls
60+
guard scrollView.panGestureRecognizer.state == .began || scrollView.panGestureRecognizer.state == .changed else {
61+
return
62+
}
63+
64+
guard let action = predicate.rumAction(targetView: scrollView) else {
65+
return // Filtered by predicate
66+
}
67+
68+
// If there's an active scroll on this view (e.g. user started dragging during deceleration),
69+
// finalize the previous one before starting a new one.
70+
finalizeScrollIfNeeded(scrollView)
71+
72+
let state = ScrollState(
73+
startTime: dateProvider.now,
74+
startOffset: scrollView.contentOffset,
75+
actionName: action.name
76+
)
77+
78+
activeScrolls[ObjectIdentifier(scrollView)] = state
79+
80+
let command = RUMStartUserActionCommand(
81+
time: state.startTime,
82+
globalAttributes: [:],
83+
attributes: action.attributes,
84+
instrumentation: .uikit,
85+
actionType: .scroll,
86+
name: action.name
87+
)
88+
89+
subscriber?.process(command: command)
90+
}
91+
92+
/// Called when dragging ends. Captures lift velocity while the gesture recognizer still has it.
93+
/// If no deceleration follows, finalizes the scroll immediately.
94+
func notify_scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
95+
let id = ObjectIdentifier(scrollView)
96+
97+
// Capture velocity now — after deceleration it will be zero
98+
let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
99+
activeScrolls[id]?.liftVelocity = velocity
100+
101+
if !decelerate {
102+
finalizeScroll(scrollView)
103+
}
104+
}
105+
106+
/// Called when deceleration completes after a fling. Finalize the gesture.
107+
func notify_scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
108+
finalizeScroll(scrollView)
109+
}
110+
111+
// MARK: - Private
112+
113+
/// Finalizes the active scroll on this view if one exists. Called before starting a new scroll
114+
/// to ensure every START has a matching STOP (e.g. when the user drags during deceleration).
115+
private func finalizeScrollIfNeeded(_ scrollView: UIScrollView) {
116+
let id = ObjectIdentifier(scrollView)
117+
118+
if activeScrolls[id] != nil {
119+
finalizeScroll(scrollView)
120+
}
121+
}
122+
123+
private func finalizeScroll(_ scrollView: UIScrollView) {
124+
let id = ObjectIdentifier(scrollView)
125+
126+
guard let state = activeScrolls.removeValue(forKey: id) else {
127+
return
128+
}
129+
130+
let velocity = state.liftVelocity ?? .zero
131+
let gestureType = classifyGesture(velocity: velocity)
132+
let direction = calculateDirection(start: state.startOffset, end: scrollView.contentOffset)
133+
134+
var attributes: [AttributeKey: AttributeValue] = [:]
135+
attributes[Self.gestureDirectionAttribute] = direction
136+
137+
let command = RUMStopUserActionCommand(
138+
time: dateProvider.now,
139+
globalAttributes: [:],
140+
attributes: attributes,
141+
actionType: gestureType,
142+
name: state.actionName
143+
)
144+
145+
subscriber?.process(command: command)
146+
}
147+
148+
private func classifyGesture(velocity: CGPoint) -> RUMActionType {
149+
let speed = sqrt(velocity.x * velocity.x + velocity.y * velocity.y)
150+
return speed >= Self.velocityThreshold ? .swipe : .scroll
151+
}
152+
153+
private func calculateDirection(start: CGPoint, end: CGPoint) -> String {
154+
let dx = end.x - start.x
155+
let dy = end.y - start.y
156+
157+
if abs(dx) > abs(dy) {
158+
return dx > 0 ? "right" : "left"
159+
} else {
160+
return dy > 0 ? "down" : "up"
161+
}
162+
}
163+
}
164+
165+
#endif

0 commit comments

Comments
 (0)