Skip to content

Instantly share code, notes, and snippets.

@saoudrizwan
Last active December 8, 2019 00:25
Show Gist options
  • Save saoudrizwan/986714d5a093f481fb3f4f6589418ea6 to your computer and use it in GitHub Desktop.
Save saoudrizwan/986714d5a093f481fb3f4f6589418ea6 to your computer and use it in GitHub Desktop.
class LinkResponsiveTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
self.delaysContentTouches = false
// required for tap to pass through on to superview & for links to work
self.isScrollEnabled = false
self.isEditable = false
self.isUserInteractionEnabled = true
self.isSelectable = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// location of the tap
var location = point
location.x -= self.textContainerInset.left
location.y -= self.textContainerInset.top
// find the character that's been tapped
let characterIndex = self.layoutManager.characterIndex(for: location, in: self.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
if characterIndex < self.textStorage.length {
// if the character is a link, handle the tap as UITextView normally would
if (self.textStorage.attribute(NSLinkAttributeName, at: characterIndex, effectiveRange: nil) != nil) {
return self
}
}
// otherwise return nil so the tap goes on to the next receiver
return nil
}
}
@rolandleth
Copy link

Hey, thanks again for the suggestion! Going to update the blog post in a bit. Here are the suggestions I mentioned:

You don't need all the self. in init.
isUserInteractionEnabled and isSelectable are true by default.
You don't need the () inside the inner if.
Pure cosmetic, but the double nested ifs would look cleaner as one and even cleaner as a guard.

@saoudrizwan
Copy link
Author

saoudrizwan commented Jun 5, 2017

Thanks @rolandleth, I just added those few bits to better inform people about what was going on. Here's the prettier version:

class LinkResponsiveTextView: UITextView {
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        delaysContentTouches = false
        isScrollEnabled = false
        isEditable = false
    }
    
    required init?(coder aDecoder: NSCoder) { fatalError() }
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let location = CGPoint(x: location.x - textContainerInset.left, y: location.y - textContainerInset.top)
        let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        if characterIndex < textStorage.length, textStorage.attribute(NSLinkAttributeName, at: characterIndex, effectiveRange: nil) != nil {
            return self
        }
        return nil
    }
}

@rolandleth
Copy link

Hey, sorry for the delay, I didn't get a notification :(

It does look better, and it's easier to scan. Two more, if you don't mind :) Probably subjective, though

Since textStorage.attribute(NSLinkAttributeName, at: characterIndex, effectiveRange: nil) != nil is such a long statement, if it'd be extracted in a constant, the if would be even easier to scan:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let location = CGPoint(x: location.x - textContainerInset.left, y: location.y - textContainerInset.top)
        let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        let characterIsPartOfURL = textStorage.attribute(NSLinkAttributeName, at: characterIndex, effectiveRange: nil) != nil

        if characterIndex < textStorage.length, characterIsPartOfURL { 
            return self 
        }

        return nil
    }

I'd usually say that using a guard would make the intent of the whole method slightly better expressed, since it's usually better to "follow the happy path", but in this case the purpose of the method can be thought of "always return nil, except on URLs".

Thanks again for the suggestion, I updated my post to include this solution as well!

@huntercasillas
Copy link

huntercasillas commented Oct 2, 2019

Hey @saoudrizwan I'm getting "Fatal error: init(coder:) has not been implemented:" any idea on how or where to implement that so that I get this class working? Thank you!!

I was able to fix the issue by replacing your required init with this:

required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment