Background:

The project needs to implement a long press pop-up optional copy content display control, the requirements are broken down:

  1. Text can be copied;
  2. Select all content by default, partial content can be selected;
  3. Identify the url, identify the phone number, click to jump/call up (same as the chat session identification rules);
  4. Underline hyperlinks;

Possible solutions include:

  1. Reuse rich text label of chat session bubble content: This scheme can directly reuse chat recognition rules and link number processing, but the disadvantage is that it needs to realize the effect of selecting part of content by long press;
  2. Using UITextView: the advantage is that the system can automatically identify hyperlinks, the system has its own long press to select the replication effect;

Considering the need for long press select replication, using UITextView scheme is more convenient.

UITextView selects the implementation of replication

The key code to implement the long-press copy effect of UITextView is to turn off the edit property

textView.editable = NO; textView.tintColor = HBGTextFieldTintColor; / / add hyperlinks. Underline textView linkTextAttributes = @ {NSForegroundColorAttributeName: HBGTextFieldTintColor, NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle)};Copy the code

textView.dataDetectorTypes = UIDataDetectorTypeLink | UIDataDetectorTypePhoneNumber; // Identify urls and numbers

Implement textView click hyperlink proxy method to handle click events:

#pragma mark -- UITextViewDelegate - (BOOL)textView:(UITextView*)textView shouldInteractWithURL:(NSURL*)URL inRange:(NSRange)characterRange { return [self handleTextViewInteractWithUrl:URL inRange:characterRange]; } - (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange Interaction: (UITextItemInteraction) interaction {if (interaction = = UITextItemInteractionInvokeDefaultAction) {/ / click gesture return [self handleTextViewInteractWithUrl:URL inRange:characterRange]; } return YES; } - (BOOL)handleTextViewInteractWithUrl:(NSURL *)URL inRange:(NSRange)characterRange { NSRange range = characterRange; for (HBGKeywordModel *model in self.keywordModels) { if (NSEqualRanges(range, model.range)) { NSURL *url; NSString *urlstring = model.url; if (! [urlstring hasPrefix:@"http://"] && ! [urlstring hasPrefix:@"https://"]) { url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", urlstring]]; } else { url = [NSURL URLWithString:urlstring]; } [self didClickLinkUrl:url]; return NO; } } for (HBGKeywordModel *model in self.phoneArr) { if (NSEqualRanges(range, model.range)) { [self didClickPhoneNumber:model.url]; return NO; } } return NO; }Copy the code

After implementing the above code, we find that there are several potholes that need to be resolved:

  1. The range of links identified by the system is inconsistent with the range identified by the library that resolves links and numbers in the project;
  2. After iOS12, apple canceled 3D Touch and changed to long press gesture, so UITextView long time if the focus is on the link, will trigger 3D Touch preview effect, rather than text content selection copy menu;
  3. Long press gesture to select text content by default;

Fill in the pits one by one.

1. System identification and custom resolution range deviation

First, turn off the system’s automatic recognition, and then set the link range and hyperlinks to parse themselves:

textView.dataDetectorTypes = UIDataDetectorTypeNone; // Turn off the system detector and manually add the hyperlink yourselfCopy the code
NSMutableAttributedString *attributeContent = [[NSMutableAttributedString alloc] initWithString:self.content]; NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; paragraphStyle.alignment = NSTextAlignmentJustified; / / text alignment aligned on both sides (alignment) [attributeContent addAttributes: @ {NSParagraphStyleAttributeName: paragraphStyle, NSFontAttributeName: [UIFont systemFontOfSize:17] } range:NSMakeRange(0, self.content.length)]; for (HBGKeywordModel *urlModel in self.keywordModels) { [attributeContent addAttribute:NSLinkAttributeName value:urlModel.url range:urlModel.range]; // Set the link yourself, Don't use the system automatically identify / / [attributeContent addAttribute: NSForegroundColorAttributeName value: HBGTextFieldTintColor range:urlModel.range]; / /! Hyperlink color this setting is invalid! } for (HBGKeywordModel *phoneModel in self.phoneArr) { BOOL shouldAddLinkText = YES; For (HBGKeywordModel *urlModel in self.keywordModels) {// Already exists in the link click text, If (NSEqualRanges(NSUnionRange(phonemodel.range, urlmodel.range), urlModel.range)) {shouldAddLinkText = NO; break; } } if (shouldAddLinkText) { [attributeContent addAttribute:NSLinkAttributeName value:phoneModel.url range:phoneModel.range]; / links/set themselves up, do not use the system automatically recognize}} the self. The textView. AttributedText = attributeContent;Copy the code

2. Long press to trigger 3D Touch preview problem

First, I haven’t found a way to turn off the UITextView hyperlink preview for a long time. Then, you want to turn off the long-press link to trigger the preview gesture. I found that there were too many gestures in the UITextView system, so I could not distinguish 😂. I added gestures to textView and printed the gestures triggered by long press. I found that about 5 gestures were triggered, but it was still difficult to distinguish which (or several) gestures triggered the 3D Touch effect. Finally, because textView is only used for text display, only the click gesture is used and none of the other gestures are used, so only the click gesture is reserved and all other gestures are disabled.

Print link click gesture:

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer { NSLog(@"otherGestureRecognizer:%@",[otherGestureRecognizer class]); return YES; } / / print: otherGestureRecognizer: UITextTapRecognizer otherGestureRecognizer: UITapGestureRecognizerCopy the code

As you can see, clicking on the link is accomplished with just two gestures UITextTapRecognizer UITapGestureRecognizer

Handling system gestures:

// In the UITextView subclass for (UIGestureRecognizer *gestureTmp in self. Gesturecognizers) {if (! [NSStringFromClass([gestureTmp class]) isEqualToString:@"UITextTapRecognizer"]  & & ! [NSStringFromClass([gestureTmp class]) isEqualToString:@"UITapGestureRecognizer"] Gesturetmp. enabled = NO; gesturetmp. enabled = NO; }}Copy the code

3. Long press gesture selects all text content by default

The default long press gesture is to select words near the focus. To selectAll by default, you cannot call selectAll: in the method triggered by the system UIMenuController, because the UIMenuController calls the method repeatedly every time it moves the selection range. You can only add your own long-press gesture to call selectAll: :

// Add custom long press gesture in UITextView subclass, To implement the default selection UILongPressGestureRecognizer * longPress = [[UILongPressGestureRecognizer alloc] initWithTarget: self action:@selector(longPressGestureRecognizer:)]; [self addGestureRecognizer:longPress]; - (void)longPressGestureRecognizer:(UILongPressGestureRecognizer *)longPressGR { UIMenuController *menuController = [UIMenuController sharedMenuController]; // selectAll by default [self selectAll:menuController]; }Copy the code

Other Settings

Sets the options displayed by UIMenuController

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { If (action == @selector(cut:)) return NO; If (action == @selector(copy:)) return YES; // select if (action == @selector(select:)) return NO; If (action == @selector(selectAll:)) return YES; Paste if (action == @selector(paste:)) return NO; If (action == @selector(delete:)) return NO; If (action == NSSelectorFromString(@"_define:")) return YES; // learn if (action == NSSelectorFromString(@"_addShortcut:")) return NO; If (action == NSSelectorFromString(@"_share:")) return YES; return [super canPerformAction:action withSender:sender]; }Copy the code