I'm using the UITextView
delegate method to do some custom work like opening a in-app browser when user tapping on URL or attachment:
func textView(_ textView: UITextView,
shouldInteractWith URL: URL,
in characterRange: NSRange,
interaction: UITextItemInteraction) -> Bool
In iOS 13, this delegate method is called even when the user is just scrolling upon the URL, which is not expected. This behavior also applied to image attachment.
That deleate method now is called via the interaction.
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 6.1 7.1
* frame #0: 0x0000000104a54c5c ProjectS1`PostListViewController.textView(textView=0x00000001090a4600, URL=Foundation.URL @ 0x000000016b5d1200, characterRange=location=161, length=9, interaction=invokeDefaultAction, self=0x0000000109b03990) at PostListViewController.swift:610:9
frame #1: 0x0000000104a54d70 ProjectS1`@objc PostListViewController.textView(_:shouldInteractWith:in:interaction:) at <compiler-generated>:0
frame #2: 0x00000001b3293eec UIKitCore`-[UITextView _allowInteraction:forTextInteractableItem:] + 212
frame #3: 0x00000001b2602160 UIKitCore`-[_UITextInteractableItem _allowInteraction:] + 140
frame #4: 0x00000001b2601f68 UIKitCore`-[_UITextInteractableItem canInvokeDefaultAction] + 100
frame #5: 0x00000001b31dd528 UIKitCore`-[_UITextSimpleLinkInteraction _canBeginInteractionSessionForLinkAtPoint:asTap:] + 136
frame #6: 0x00000001b31dd3d0 UIKitCore`-[_UITextSimpleLinkInteraction interaction_gestureRecognizer:shouldReceiveTouch:] + 228
frame #7: 0x00000001b31dc234 UIKitCore`-[UITextInteraction gestureRecognizer:shouldReceiveTouch:] + 144
frame #8: 0x00000001b2b5f460 UIKitCore`-[UIGestureRecognizer _delegateShouldReceiveTouch:] + 452
frame #9: 0x00000001b2b5edf4 UIKitCore`-[UIGestureRecognizer _shouldReceiveTouch:forEvent:recognizerView:] + 488
frame #10: 0x00000001b2ffa630 UIKitCore`__56-[UITouchesEvent _addGestureRecognizersForView:toTouch:]_block_invoke + 332
frame #11: 0x00000001b2ffa0e4 UIKitCore`__62-[UITouchesEvent _collectGestureRecognizersForView:withBlock:]_block_invoke + 408
frame #12: 0x00000001b2ff9b58 UIKitCore`-[UITouchesEvent _collectGestureRecognizersForView:withBlock:] + 308
frame #13: 0x00000001b2ffa4b0 UIKitCore`-[UITouchesEvent _addGestureRecognizersForView:toTouch:] + 164
frame #14: 0x00000001b2ffa9a8 UIKitCore`-[UITouchesEvent _addTouch:forDelayedDelivery:] + 812
frame #15: 0x00000001b300bfac UIKitCore`_AddTouchToEventAndDetermineIfNeedsCancel + 196
frame #16: 0x00000001b300c074 UIKitCore`____updateTouchesWithDigitizerEventAndDetermineIfShouldSend_block_invoke.96 + 136
frame #17: 0x00000001aef35b20 CoreFoundation`__NSDICTIONARY_IS_CALLING_OUT_TO_A_BLOCK__ + 24
frame #18: 0x00000001aef360e4 CoreFoundation`____NSDictionaryEnumerate_block_invoke.11 + 56
frame #19: 0x00000001aef07a10 CoreFoundation`CFBasicHashApply + 144
frame #20: 0x00000001aef35c80 CoreFoundation`__NSDictionaryEnumerate + 220
frame #21: 0x00000001b300d560 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 2444
frame #22: 0x00000001b30107dc UIKitCore`__handleEventQueueInternal + 4928
frame #23: 0x00000001b3009960 UIKitCore`__handleHIDEventFetcherDrain + 112
frame #24: 0x00000001aee61260 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 28
frame #25: 0x00000001aee611b4 CoreFoundation`__CFRunLoopDoSource0 + 84
frame #26: 0x00000001aee60920 CoreFoundation`__CFRunLoopDoSources0 + 184
frame #27: 0x00000001aee5b7ec CoreFoundation`__CFRunLoopRun + 1068
frame #28: 0x00000001aee5b098 CoreFoundation`CFRunLoopRunSpecific + 480
frame #29: 0x00000001b8fc5534 GraphicsServices`GSEventRunModal + 108
frame #30: 0x00000001b2f7b7ac UIKitCore`UIApplicationMain + 1940
frame #31: 0x0000000104b090d0 ProjectS1`main at AppDelegate.swift:25:7
frame #32: 0x00000001aecdaf30 libdyld.dylib`start + 4
So my question is that, is there any way to know whether user is tapping on the URL or just scrolling on the URL?
It looks like textView:shouldInteractWithURL:inRange:interaction:
ends up being called 3 times during a normal link press (if you always return YES
).
A couple of times to see if the default action can be invoked (assuming that's what canInvokeDefaultAction
is about):
* frame #0: 0x0000000101610038 Engage`::-[BubbleMessageCell textView:shouldInteractWithURL:inRange:interaction:](self=0x00007ff19e999600, _cmd="textView:shouldInteractWithURL:inRange:interaction:", textView=0x00007ff19eaf2000, url="https://9to5mac.com/2019/09/07/imessage-for-business/", characterRange=location=235, length=33, interaction=UITextItemInteractionInvokeDefaultAction) at BubbleMessageCell.mm:623:5
frame #1: 0x00007fff478b2902 UIKitCore`-[UITextView _allowInteraction:forTextInteractableItem:] + 532
frame #2: 0x00007fff46b8ba1c UIKitCore`-[_UITextInteractableItem _allowInteraction:] + 135
frame #3: 0x00007fff46b8b84d UIKitCore`-[_UITextInteractableItem canInvokeDefaultAction] + 97
frame #4: 0x00007fff477f76f7 UIKitCore`-[_UITextSimpleLinkInteraction _canBeginInteractionSessionForLinkAtPoint:asTap:] + 127
frame #5: 0x00007fff477f75c7 UIKitCore`-[_UITextSimpleLinkInteraction interaction_gestureRecognizer:shouldReceiveTouch:] + 217
frame #6: 0x00007fff477f6506 UIKitCore`-[UITextInteraction gestureRecognizer:shouldReceiveTouch:] + 127
frame #7: 0x00007fff47133669 UIKitCore`-[UIGestureRecognizer _delegateShouldReceiveTouch:] + 493
* frame #0: 0x0000000101610038 Engage`::-[BubbleMessageCell textView:shouldInteractWithURL:inRange:interaction:](self=0x00007ff19e999600, _cmd="textView:shouldInteractWithURL:inRange:interaction:", textView=0x00007ff19eaf2000, url="https://9to5mac.com/2019/09/07/imessage-for-business/", characterRange=location=235, length=33, interaction=UITextItemInteractionInvokeDefaultAction) at BubbleMessageCell.mm:623:5
frame #1: 0x00007fff478b2902 UIKitCore`-[UITextView _allowInteraction:forTextInteractableItem:] + 532
frame #2: 0x00007fff46b8ba1c UIKitCore`-[_UITextInteractableItem _allowInteraction:] + 135
frame #3: 0x00007fff46b8b84d UIKitCore`-[_UITextInteractableItem canInvokeDefaultAction] + 97
frame #4: 0x00007fff477f77fb UIKitCore`-[_UITextSimpleLinkInteraction _beginInteractionSessionForLinkAtPoint:asTap:] + 167
frame #5: 0x00007fff477f74d0 UIKitCore`-[_UITextSimpleLinkInteraction interaction_gestureRecognizerShouldBegin:] + 196
frame #6: 0x00007fff477f62a1 UIKitCore`-[UITextInteraction gestureRecognizerShouldBegin:] + 307
frame #7: 0x00007fff471339b6 UIKitCore`-[UIGestureRecognizer _shouldBegin] + 413
And then finally when the gesture is actually recognized:
* frame #0: 0x0000000101610038 Engage`::-[BubbleMessageCell textView:shouldInteractWithURL:inRange:interaction:](self=0x00007ff19e999600, _cmd="textView:shouldInteractWithURL:inRange:interaction:", textView=0x00007ff19eaf2000, url="https://9to5mac.com/2019/09/07/imessage-for-business/", characterRange=location=235, length=33, interaction=UITextItemInteractionInvokeDefaultAction) at BubbleMessageCell.mm:623:5
frame #1: 0x00007fff478b2902 UIKitCore`-[UITextView _allowInteraction:forTextInteractableItem:] + 532
frame #2: 0x00007fff46b8ba1c UIKitCore`-[_UITextInteractableItem _allowInteraction:] + 135
frame #3: 0x00007fff46b8b8cc UIKitCore`-[_UITextInteractableItem invokeDefaultAction] + 94
frame #4: 0x00007fff477f6f32 UIKitCore`-[_UITextSimpleLinkInteraction linkTapped:] + 188
frame #5: 0x00007fff4712bbfb UIKitCore`-[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] + 44
When scrolling, only the first call happens.
This is presumably an iOS 13.1 change, where it checks sooner to see if the link can be interacted with. If you want textView:shouldInteractWithURL:inRange:interaction:
to have side effects, you only want to do them when the gesture is actually recognized.
What appears to work for us to to check textView.gestureRecognizers
and only do the custom actions if a tap gesture was being recognized.
BOOL recognizedTapGesture = NO;
for (UIGestureRecognizer *recognizer in textView.gestureRecognizers) {
if ([recognizer isKindOfClass:UITapGestureRecognizer.class] && recognizer.state == UIGestureRecognizerStateEnded) {
recognizedTapGesture = YES;
break;
}
}
if (!recognizedTapGesture) {
// Tap gesture is not being recognized, this must be an early
// check when touches begin. Leave the link handling alone.
return YES;
}
// Do custom action here
return NO;
I just encountered the same frustrating issue in iOS 13. Here is a fix that worked for me in Swift inspired by Mihai's answer above.
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
switch interaction {
case .invokeDefaultAction:
if textView.gestureRecognizers?.contains(where: {$0.isKind(of: UITapGestureRecognizer.self) && $0.state == .ended}) == true {
// Handle your custom logic here.
return false
}
return true
case .presentActions:
// Default action.
return true
case .preview:
// Default action.
return true
@unknown default:
fatalError()
}
}