@@ -40,6 +40,16 @@ typedef id (*RunnerDragPointerPathFactory)(
4040static NSString * _Nullable RunnerRequireClass (Class cls, NSString *className);
4141static NSString * _Nullable RunnerRequireSelector (Class cls, SEL selector, NSString *selectorName);
4242static NSString * _Nullable RunnerRequireApplicationSelector (id application, SEL selector, NSString *selectorName);
43+ static NSString * _Nullable RunnerCreateEventRecord (
44+ id application,
45+ NSString *recordName,
46+ RunnerXCTestEventBridge *bridge,
47+ id *record
48+ );
49+ static NSString * _Nullable RunnerSynthesizeEventRecord (
50+ const RunnerXCTestEventBridge *bridge,
51+ id record
52+ );
4353static id RunnerSwipePointerPath (
4454 const RunnerXCTestEventBridge *bridge,
4555 CGPoint start,
@@ -60,6 +70,7 @@ static id RunnerContinuousDragPointerPath(
6070 NSString *recordName,
6171 RunnerDragPointerPathFactory pathFactory
6272);
73+ static NSString * _Nullable RunnerTrySynthesizeTap (id application, CGPoint point);
6374// XCTest's proven swipe profile reaches the endpoint in 100 ms, then holds for the planned
6475// fling duration. Fast movement is what lets UIKit distinguish a fling from a timed pan.
6576static const NSTimeInterval RunnerSwipeMovementDurationSeconds = 0.1 ;
@@ -118,7 +129,7 @@ + (NSString * _Nullable)synthesizeTapWithApplication:(id)application
118129 x : (double )x
119130 y : (double )y {
120131 @try {
121- return [ self trySynthesizeTapWithApplication: application x: x y: y] ;
132+ return RunnerTrySynthesizeTap ( application, CGPointMake (x, y)) ;
122133 } @catch (NSException *exception) {
123134 NSString *name = exception.name ?: @" NSException" ;
124135 NSString *reason = exception.reason ?: @" private XCTest event synthesis failed" ;
@@ -130,27 +141,14 @@ + (NSString * _Nullable)synthesizeGestureWithApplication:(id)application
130141 pointerSamples : (NSArray <NSArray<NSDictionary<NSString *, NSNumber *> *> *> *)pointerSamples {
131142 @try {
132143 RunnerXCTestEventBridge bridge;
133- NSString *missing = RunnerResolveXCTestEventBridge (application, &bridge);
134- if (missing != nil ) return missing;
135-
136- NSInteger interfaceOrientation =
137- ((RunnerMsgSendInteger)objc_msgSend)(application, bridge.interfaceOrientationSelector );
138- NSInteger targetProcessID =
139- ((RunnerMsgSendInteger)objc_msgSend)(application, bridge.processIDSelector );
140- if (targetProcessID <= 0 ) {
141- return @" private XCTest event synthesis unavailable: could not resolve target process ID" ;
142- }
143-
144- id record = ((RunnerMsgSendInitRecord)objc_msgSend)(
145- [bridge.recordClass alloc ],
146- bridge.initRecordSelector ,
144+ id record = nil ;
145+ NSString *error = RunnerCreateEventRecord (
146+ application,
147147 @" agent-device-gesture-plan" ,
148- interfaceOrientation
148+ &bridge,
149+ &record
149150 );
150- if (record == nil ) {
151- return @" private XCTest event synthesis failed: could not create event record" ;
152- }
153- ((RunnerMsgSendSetInteger)objc_msgSend)(record, bridge.setTargetProcessIDSelector , targetProcessID);
151+ if (error != nil ) return error;
154152
155153 for (NSArray <NSDictionary <NSString *, NSNumber *> *> *samples in pointerSamples) {
156154 NSDictionary <NSString *, NSNumber *> *first = samples.firstObject ;
@@ -184,13 +182,7 @@ + (NSString * _Nullable)synthesizeGestureWithApplication:(id)application
184182 ((RunnerMsgSendAddPath)objc_msgSend)(record, bridge.addPathSelector , path);
185183 }
186184
187- NSError *error = nil ;
188- BOOL ok = ((RunnerMsgSendSynthesize)objc_msgSend)(record, bridge.synthesizeSelector , &error);
189- if (!ok) {
190- NSString *detail = error.localizedDescription ?: @" synthesizeWithError returned false" ;
191- return [NSString stringWithFormat: @" private XCTest event synthesis failed: %@ " , detail];
192- }
193- return nil ;
185+ return RunnerSynthesizeEventRecord (&bridge, record);
194186 } @catch (NSException *exception) {
195187 NSString *name = exception.name ?: @" NSException" ;
196188 NSString *reason = exception.reason ?: @" private XCTest event synthesis failed" ;
@@ -215,84 +207,36 @@ + (NSInteger)interfaceOrientationForApplication:(id)application {
215207 RunnerDragPointerPathFactory pathFactory
216208) {
217209 RunnerXCTestEventBridge bridge;
218- NSString *missing = RunnerResolveXCTestEventBridge (application, &bridge);
219- if (missing != nil ) {
220- return missing;
221- }
222-
223- NSInteger interfaceOrientation =
224- ((RunnerMsgSendInteger)objc_msgSend)(application, bridge.interfaceOrientationSelector );
225- NSInteger targetProcessID = ((RunnerMsgSendInteger)objc_msgSend)(application, bridge.processIDSelector );
226- if (targetProcessID <= 0 ) {
227- return @" private XCTest event synthesis unavailable: could not resolve target process ID" ;
228- }
229-
230- id record = ((RunnerMsgSendInitRecord)objc_msgSend)(
231- [bridge.recordClass alloc ],
232- bridge.initRecordSelector ,
233- recordName,
234- interfaceOrientation
235- );
236- if (record == nil ) {
237- return @" private XCTest event synthesis failed: could not create event record" ;
238- }
239- ((RunnerMsgSendSetInteger)objc_msgSend)(record, bridge.setTargetProcessIDSelector , targetProcessID);
210+ id record = nil ;
211+ NSString *error = RunnerCreateEventRecord (application, recordName, &bridge, &record);
212+ if (error != nil ) return error;
240213
241214 id path = pathFactory (&bridge, start, end, durationMs);
242215 if (path == nil ) {
243216 return @" private XCTest event synthesis failed: could not create pointer path" ;
244217 }
245218 ((RunnerMsgSendAddPath)objc_msgSend)(record, bridge.addPathSelector , path);
246219
247- NSError *error = nil ;
248- BOOL ok = ((RunnerMsgSendSynthesize)objc_msgSend)(record, bridge.synthesizeSelector , &error);
249- if (!ok) {
250- NSString *detail = error.localizedDescription ?: @" synthesizeWithError returned false" ;
251- return [NSString stringWithFormat: @" private XCTest event synthesis failed: %@ " , detail];
252- }
253- return nil ;
220+ return RunnerSynthesizeEventRecord (&bridge, record);
254221}
255222
256- + (NSString * _Nullable)trySynthesizeTapWithApplication : (id )application
257- x : (double )x
258- y : (double )y {
223+ static NSString * _Nullable RunnerTrySynthesizeTap (id application, CGPoint point) {
259224 RunnerXCTestEventBridge bridge;
260- NSString *missing = RunnerResolveXCTestEventBridge (application, &bridge);
261- if (missing != nil ) {
262- return missing;
263- }
264-
265- NSInteger interfaceOrientation =
266- ((RunnerMsgSendInteger)objc_msgSend)(application, bridge.interfaceOrientationSelector );
267- NSInteger targetProcessID = ((RunnerMsgSendInteger)objc_msgSend)(application, bridge.processIDSelector );
268- if (targetProcessID <= 0 ) {
269- return @" private XCTest event synthesis unavailable: could not resolve target process ID" ;
270- }
271-
272- id record = ((RunnerMsgSendInitRecord)objc_msgSend)(
273- [bridge.recordClass alloc ],
274- bridge.initRecordSelector ,
225+ id record = nil ;
226+ NSString *error = RunnerCreateEventRecord (
227+ application,
275228 @" agent-device-tap" ,
276- interfaceOrientation
229+ &bridge,
230+ &record
277231 );
278- if (record == nil ) {
279- return @" private XCTest event synthesis failed: could not create event record" ;
280- }
281- ((RunnerMsgSendSetInteger)objc_msgSend)(record, bridge.setTargetProcessIDSelector , targetProcessID);
232+ if (error != nil ) return error;
282233
283- id path = RunnerTapPointerPath (&bridge, CGPointMake (x, y) );
234+ id path = RunnerTapPointerPath (&bridge, point );
284235 if (path == nil ) {
285236 return @" private XCTest event synthesis failed: could not create pointer path" ;
286237 }
287238 ((RunnerMsgSendAddPath)objc_msgSend)(record, bridge.addPathSelector , path);
288-
289- NSError *error = nil ;
290- BOOL ok = ((RunnerMsgSendSynthesize)objc_msgSend)(record, bridge.synthesizeSelector , &error);
291- if (!ok) {
292- NSString *detail = error.localizedDescription ?: @" synthesizeWithError returned false" ;
293- return [NSString stringWithFormat: @" private XCTest event synthesis failed: %@ " , detail];
294- }
295- return nil ;
239+ return RunnerSynthesizeEventRecord (&bridge, record);
296240}
297241
298242static NSString * _Nullable RunnerResolveXCTestEventBridge (
@@ -382,6 +326,53 @@ + (NSString * _Nullable)trySynthesizeTapWithApplication:(id)application
382326 return nil ;
383327}
384328
329+ static NSString * _Nullable RunnerCreateEventRecord (
330+ id application,
331+ NSString *recordName,
332+ RunnerXCTestEventBridge *bridge,
333+ id *record
334+ ) {
335+ NSString *missing = RunnerResolveXCTestEventBridge (application, bridge);
336+ if (missing != nil ) return missing;
337+
338+ NSInteger interfaceOrientation =
339+ ((RunnerMsgSendInteger)objc_msgSend)(application, bridge->interfaceOrientationSelector );
340+ NSInteger targetProcessID =
341+ ((RunnerMsgSendInteger)objc_msgSend)(application, bridge->processIDSelector );
342+ if (targetProcessID <= 0 ) {
343+ return @" private XCTest event synthesis unavailable: could not resolve target process ID" ;
344+ }
345+
346+ id eventRecord = ((RunnerMsgSendInitRecord)objc_msgSend)(
347+ [bridge->recordClass alloc ],
348+ bridge->initRecordSelector ,
349+ recordName,
350+ interfaceOrientation
351+ );
352+ if (eventRecord == nil ) {
353+ return @" private XCTest event synthesis failed: could not create event record" ;
354+ }
355+ ((RunnerMsgSendSetInteger)objc_msgSend)(
356+ eventRecord,
357+ bridge->setTargetProcessIDSelector ,
358+ targetProcessID
359+ );
360+ *record = eventRecord;
361+ return nil ;
362+ }
363+
364+ static NSString * _Nullable RunnerSynthesizeEventRecord (
365+ const RunnerXCTestEventBridge *bridge,
366+ id record
367+ ) {
368+ NSError *error = nil ;
369+ BOOL ok = ((RunnerMsgSendSynthesize)objc_msgSend)(record, bridge->synthesizeSelector , &error);
370+ if (!ok) {
371+ NSString *detail = error.localizedDescription ?: @" synthesizeWithError returned false" ;
372+ return [NSString stringWithFormat: @" private XCTest event synthesis failed: %@ " , detail];
373+ }
374+ return nil ;
375+ }
385376
386377static id RunnerSwipePointerPath (
387378 const RunnerXCTestEventBridge *bridge,
@@ -422,6 +413,9 @@ static id RunnerContinuousDragPointerPath(
422413 return nil ;
423414 }
424415
416+ // This is velocity shaping, not just interpolation density: smoothstep's endpoint slope is zero,
417+ // while a planned linear segment reaches lift with nonzero velocity unless a destination hold
418+ // follows it. UIKit uses finger-up velocity for scroll deceleration. See ADR 0013 and issue #1586.
425419 int frameCount = MAX (3 , (int )(durationMs / 16.0 ));
426420 NSTimeInterval durationSeconds = durationMs / 1000.0 ;
427421 for (int index = 1 ; index <= frameCount; index += 1 ) {
0 commit comments