2020-08-21 04:29:01 +02:00
|
|
|
// Copyright © 2020 Metabolist. All rights reserved.
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
|
2020-09-08 04:35:28 +02:00
|
|
|
final class TouchFallthroughTextView: UITextView {
|
2020-08-21 04:29:01 +02:00
|
|
|
var shouldFallthrough: Bool = true
|
|
|
|
|
2020-09-30 08:00:04 +02:00
|
|
|
private var linkHighlightView: UIView?
|
|
|
|
|
2020-08-21 04:29:01 +02:00
|
|
|
override init(frame: CGRect, textContainer: NSTextContainer?) {
|
|
|
|
super.init(frame: frame, textContainer: textContainer)
|
2020-10-13 22:11:27 +02:00
|
|
|
|
|
|
|
clipsToBounds = false
|
|
|
|
textDragInteraction?.isEnabled = false
|
2020-10-25 07:45:45 +01:00
|
|
|
isEditable = false
|
2021-01-24 23:12:04 +01:00
|
|
|
isScrollEnabled = false
|
|
|
|
delaysContentTouches = false
|
2020-10-13 22:11:27 +02:00
|
|
|
textContainerInset = .zero
|
|
|
|
self.textContainer.lineFragmentPadding = 0
|
|
|
|
linkTextAttributes = [.foregroundColor: tintColor as Any, .underlineColor: UIColor.clear]
|
2020-08-21 04:29:01 +02:00
|
|
|
}
|
|
|
|
|
2020-10-13 22:11:27 +02:00
|
|
|
@available(*, unavailable)
|
2020-08-21 04:29:01 +02:00
|
|
|
required init?(coder: NSCoder) {
|
2020-10-13 22:11:27 +02:00
|
|
|
fatalError("init(coder:) has not been implemented")
|
2020-08-21 04:29:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
2021-02-02 23:33:54 +01:00
|
|
|
guard !UIAccessibility.isVoiceOverRunning else { return super.point(inside: point, with: event) }
|
|
|
|
|
|
|
|
return shouldFallthrough ? urlAndRect(at: point) != nil : super.point(inside: point, with: event)
|
2020-08-21 04:29:01 +02:00
|
|
|
}
|
|
|
|
|
2020-09-30 08:00:04 +02:00
|
|
|
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
|
|
super.touchesBegan(touches, with: event)
|
|
|
|
|
|
|
|
guard let touch = touches.first,
|
|
|
|
let (_, rect) = urlAndRect(at: touch.location(in: self)) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let linkHighlightView = UIView(frame: rect)
|
|
|
|
|
|
|
|
self.linkHighlightView = linkHighlightView
|
|
|
|
linkHighlightView.transform = Self.linkHighlightViewTransform
|
|
|
|
linkHighlightView.layer.cornerRadius = .defaultCornerRadius
|
|
|
|
linkHighlightView.backgroundColor = .secondarySystemBackground
|
|
|
|
insertSubview(linkHighlightView, at: 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
|
|
super.touchesEnded(touches, with: event)
|
|
|
|
|
|
|
|
removeLinkHighlightView()
|
|
|
|
}
|
|
|
|
|
|
|
|
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
|
|
super.touchesCancelled(touches, with: event)
|
|
|
|
|
|
|
|
removeLinkHighlightView()
|
|
|
|
}
|
|
|
|
|
2020-08-21 04:29:01 +02:00
|
|
|
override var selectedTextRange: UITextRange? {
|
|
|
|
get { shouldFallthrough ? nil : super.selectedTextRange }
|
|
|
|
set {
|
|
|
|
if !shouldFallthrough {
|
|
|
|
super.selectedTextRange = newValue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override var intrinsicContentSize: CGSize {
|
2020-12-03 23:32:15 +01:00
|
|
|
return text.isEmpty ? .zero : super.intrinsicContentSize
|
2020-08-21 04:29:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func urlAndRect(at point: CGPoint) -> (URL, CGRect)? {
|
|
|
|
guard
|
|
|
|
let pos = closestPosition(to: point),
|
|
|
|
let range = tokenizer.rangeEnclosingPosition(
|
|
|
|
pos, with: .character,
|
|
|
|
inDirection: UITextDirection.layout(.left))
|
|
|
|
else { return nil }
|
|
|
|
|
|
|
|
let urlAtPointIndex = offset(from: beginningOfDocument, to: range.start)
|
|
|
|
|
|
|
|
guard let url = attributedText.attribute(
|
|
|
|
.link, at: offset(from: beginningOfDocument, to: range.start),
|
|
|
|
effectiveRange: nil) as? URL
|
|
|
|
else { return nil }
|
|
|
|
|
|
|
|
let maxLength = attributedText.length
|
|
|
|
var min = urlAtPointIndex
|
|
|
|
var max = urlAtPointIndex
|
|
|
|
|
|
|
|
attributedText.enumerateAttribute(
|
|
|
|
.link,
|
|
|
|
in: NSRange(location: 0, length: urlAtPointIndex),
|
|
|
|
options: .reverse) { attribute, range, stop in
|
|
|
|
if let attributeURL = attribute as? URL, attributeURL == url, min > 0 {
|
|
|
|
min = range.location
|
|
|
|
} else {
|
|
|
|
stop.pointee = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
attributedText.enumerateAttribute(
|
|
|
|
.link,
|
|
|
|
in: NSRange(location: urlAtPointIndex, length: maxLength - urlAtPointIndex),
|
|
|
|
options: []) { attribute, range, stop in
|
|
|
|
if let attributeURL = attribute as? URL, attributeURL == url, max < maxLength {
|
|
|
|
max = range.location + range.length
|
|
|
|
} else {
|
|
|
|
stop.pointee = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var urlRect = CGRect.zero
|
|
|
|
|
|
|
|
layoutManager.enumerateEnclosingRects(
|
|
|
|
forGlyphRange: NSRange(location: min, length: max - min),
|
|
|
|
withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0),
|
|
|
|
in: textContainer) { rect, _ in
|
|
|
|
if urlRect.origin == .zero {
|
|
|
|
urlRect.origin = rect.origin
|
|
|
|
}
|
|
|
|
|
|
|
|
urlRect = urlRect.union(rect)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (url, urlRect)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension TouchFallthroughTextView {
|
2020-09-30 08:00:04 +02:00
|
|
|
static let linkHighlightViewTransform = CGAffineTransform(scaleX: 1.1, y: 1.1)
|
|
|
|
|
|
|
|
func removeLinkHighlightView() {
|
|
|
|
UIView.animate(withDuration: .defaultAnimationDuration) {
|
|
|
|
self.linkHighlightView?.alpha = 0
|
|
|
|
} completion: { _ in
|
|
|
|
self.linkHighlightView?.removeFromSuperview()
|
|
|
|
self.linkHighlightView = nil
|
|
|
|
}
|
|
|
|
}
|
2020-08-21 04:29:01 +02:00
|
|
|
}
|