Skip to content

Instantly share code, notes, and snippets.

@jawj
Last active January 10, 2020 12:34
Show Gist options
  • Save jawj/9773c827e26a9bac32fe199f8cc00fb1 to your computer and use it in GitHub Desktop.
Save jawj/9773c827e26a9bac32fe199f8cc00fb1 to your computer and use it in GitHub Desktop.
UITextView with tappable links even when not selectable
//
// LinkableTextView.h
//
// Created by George MacKerron on 2020/01/09.
// Copyright © 2020 George MacKerron. MIT licenced.
//
@import UIKit;
@interface LinkableTextView : UITextView
@property (nonatomic) NSDictionary* activeLinkTextAttributes;
@property (nonatomic) NSURL* activeLink;
@property (nonatomic) NSRange activeLinkRange;
@property (nonatomic) NSArray<UITextSelectionRect*>* activeLinkRects;
@end
//
// LinkableTextView.m
//
// Created by George MacKerron on 2020/01/09.
// Copyright © 2020 George MacKerron. MIT licenced
//
#import "LinkableTextView.h"
#define MaxMovementFromLink 64.0
#define ActiveLinkOpacity 0.5
CGFloat squaredDistanceFromPointToRect(CGPoint pt, CGRect rect) {
// explanation: https://stackoverflow.com/a/18157551/338196
CGFloat dx = fmax(0, fmax(rect.origin.x - pt.x, pt.x - (rect.origin.x + rect.size.width)));
CGFloat dy = fmax(0, fmax(rect.origin.y - pt.y, pt.y - (rect.origin.y + rect.size.height)));
return dx * dx + dy * dy;
}
@implementation LinkableTextView
- (void)setActiveLink:(id)link range:(NSRange)range {
// called with a NSString or NSURL link (and NSRange range) to activate a link
// called with a nil link (and any old NSRange) to deactivate the active link
if (! self.activeLinkTextAttributes) { // set some default attributes if none provided
UIColor* linkColor = self.linkTextAttributes[NSForegroundColorAttributeName];
self.activeLinkTextAttributes = @{NSForegroundColorAttributeName:
[linkColor colorWithAlphaComponent:ActiveLinkOpacity]};
};
NSMutableAttributedString* s = self.attributedText.mutableCopy;
if (link) {
[s removeAttribute:NSLinkAttributeName range:range]; // must remove the link, or its style takes precedence
[s addAttributes:self.activeLinkTextAttributes range:range];
} else {
[s removeAttribute:NSForegroundColorAttributeName range:self.activeLinkRange];
[s addAttribute:NSLinkAttributeName value:self.activeLink range:self.activeLinkRange];
}
self.attributedText = s;
self.activeLink = [link isKindOfClass:NSString.class] ? [NSURL URLWithString:link] : link;
self.activeLinkRange = range;
if (self.activeLink) {
UITextPosition* posStart = [self positionFromPosition:self.beginningOfDocument
offset:self.activeLinkRange.location];
UITextPosition* posEnd = [self positionFromPosition:posStart offset:self.activeLinkRange.length];
UITextRange* textRange = [self textRangeFromPosition:posStart toPosition:posEnd];
self.activeLinkRects = [self selectionRectsForRange:textRange];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (self.activeLink) [self setActiveLink:nil range:NSMakeRange(0, 0)];
UITouch* touch = touches.anyObject;
CGPoint touchPoint = [touch locationInView:self];
UITextRange* tapRange = [self characterRangeAtPoint:touchPoint];
NSInteger start = [self offsetFromPosition:self.beginningOfDocument toPosition:tapRange.start];
NSInteger length = [self offsetFromPosition:tapRange.start toPosition:tapRange.end];
NSRange targetRange = NSMakeRange(start, length);
NSRange totalRange = NSMakeRange(0, self.attributedText.length);
[self.attributedText enumerateAttribute:NSLinkAttributeName inRange:totalRange options:kNilOptions
usingBlock:^(id _Nullable link, NSRange range, BOOL * _Nonnull stop) {
if (link && NSIntersectionRange(range, targetRange).length > 0) {
[self setActiveLink:link range:range];
*stop = YES;
}
}];
if (! self.activeLink) [super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (! self.activeLink) {
[super touchesMoved:touches withEvent:event];
return;
}
CGPoint touchPoint = [touches.anyObject locationInView:self];
CGFloat minSquaredDistance = DBL_MAX;
for (UITextSelectionRect* rect in self.activeLinkRects) minSquaredDistance =
fmin(minSquaredDistance, squaredDistanceFromPointToRect(touchPoint, rect.rect));
if (minSquaredDistance < MaxMovementFromLink * MaxMovementFromLink) return;
[self setActiveLink:nil range:NSMakeRange(0, 0)];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (! self.activeLink) {
[super touchesEnded:touches withEvent:event];
return;
}
NSURL* link = self.activeLink;
[self setActiveLink:nil range:NSMakeRange(0, 0)];
[UIApplication.sharedApplication openURL:link];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (! self.activeLink) {
[super touchesCancelled:touches withEvent:event];
return;
}
[self setActiveLink:nil range:NSMakeRange(0, 0)];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment