UITextView: отключить выделение, разрешить ссылки
У меня есть UITextView
, в котором отображается NSAttributedString
. Для свойств textView editable
и selectable
установлено значение false
.
Приписанная строка содержит URL-адрес, и я хотел бы разрешить нажимать URL-адрес, чтобы открыть браузер. Но взаимодействие с URL-адресом возможно только в том случае, если для атрибута selectable
установлено значение true
.
Как я могу разрешить взаимодействие с пользователем только для ссылки на ссылки, но не для выбора текста?
Ответы
Ответ 1
Я нахожу концепцию возиться с внутренними распознавателями жестов немного пугающей, поэтому попытался найти другое решение. Я обнаружил, что мы можем переопределить point(inside:with:)
чтобы эффективно разрешить "сквозной переход", когда пользователь не касается текста со ссылкой внутри него:
// Inside a UITextView subclass:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard let pos = closestPosition(to: point) else { return false }
guard let range = tokenizer.rangeEnclosingPosition(pos, with: .character, inDirection: .layout(.left)) else { return false }
let startIndex = offset(from: beginningOfDocument, to: range.start)
return attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil
}
Это также означает, что если у вас есть UITextView
со ссылкой внутри UITableViewCell
, tableView(didSelectRowAt:)
прежнему tableView(didSelectRowAt:)
при нажатии на несвязанную часть текста :)
Ответ 2
Как сказал Cœur, вы можете UITextView
подкласс UITextView
переопределяющий метод selectedTextRange
, установив для него значение nil. И ссылки по-прежнему будут кликабельными, но вы не сможете выделить остальную часть текста.
class CustomTextView: UITextView {
override public var selectedTextRange: UITextRange? {
get {
return nil
}
set { }
}
Ответ 3
Поэтому после некоторого исследования я смог найти решение. Это хак, и я не знаю, сработает ли это в будущих версиях iOS, но работает на данный момент (iOS 9.3).
Просто добавьте эту категорию UITextView
(Gist здесь):
@implementation UITextView (NoFirstResponder)
- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer {
if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
@try {
id targetAndAction = ((NSMutableArray *)[gestureRecognizer valueForKey:@"_targets"]).firstObject;
NSArray <NSString *>*actions = @[@"action=loupeGesture:", // link: no, selection: shows circle loupe and blue selectors for a second
@"action=longDelayRecognizer:", // link: no, selection: no
/*@"action=smallDelayRecognizer:", // link: yes (no long press), selection: no*/
@"action=oneFingerForcePan:", // link: no, selection: shows rectangular loupe for a second, no blue selectors
@"action=_handleRevealGesture:"]; // link: no, selection: no
for (NSString *action in actions) {
if ([[targetAndAction description] containsString:action]) {
[gestureRecognizer setEnabled:false];
}
}
}
@catch (NSException *e) {
}
@finally {
[super addGestureRecognizer: gestureRecognizer];
}
}
}
Ответ 4
если ваша минимальная цель развертывания - iOS 11.2 или новее
Вы можете отключить выделение текста, UITextView
подкласс UITextView
и запретив жесты, которые могут что-то выбрать.
Ниже приведено решение:
- совместим с isEditable
- совместим с isScrollEnabled
- совместим со ссылками
/// Class to allow links but no selection.
/// Basically, it disables unwanted UIGestureRecognizer from UITextView.
/// https://stackoverflow.com/a/49443814/1033581
class UnselectableTappableTextView: UITextView {
// required to prevent blue background selection from any situation
override var selectedTextRange: UITextRange? {
get { return nil }
set {}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer {
// required for compatibility with isScrollEnabled
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
if let tapGestureRecognizer = gestureRecognizer as? UITapGestureRecognizer,
tapGestureRecognizer.numberOfTapsRequired == 1 {
// required for compatibility with links
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
// allowing smallDelayRecognizer for links
// /questions/725485/xcode-9-uitextview-links-no-longer-clickable
if let longPressGestureRecognizer = gestureRecognizer as? UILongPressGestureRecognizer,
// comparison value is used to distinguish between 0.12 (smallDelayRecognizer) and 0.5 (textSelectionForce and textLoupe)
longPressGestureRecognizer.minimumPressDuration < 0.325 {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
// preventing selection from loupe/magnifier (_UITextSelectionForceGesture), multi tap, tap and a half, etc.
gestureRecognizer.isEnabled = false
return false
}
}
если ваша минимальная цель развертывания - iOS 11.1 или старше
Родные средства распознавания жестов ссылок UITextView не работают на iOS 11.0-11.1 и требуют небольшой задержки долгого нажатия вместо нажатия: ссылки Xcode 9 UITextView больше не активируются
Вы можете надлежащим образом поддерживать ссылки с помощью собственного распознавателя жестов, а также можете отключить выделение текста, UITextView
подкласс UITextView
и запретив жесты, которые могут выделять что-либо или UITextView
чего-либо.
Приведенное ниже решение запретит выбор и будет:
- совместим с isScrollEnabled
- совместим со ссылками
- Обходные ограничения iOS 11.0 и iOS 11.1, но теряет эффект пользовательского интерфейса при нажатии на текстовые вложения
/// Class to support links and to disallow selection.
/// It disables most UIGestureRecognizer from UITextView and adds a UITapGestureRecognizer.
/// https://stackoverflow.com/a/49443814/1033581
class UnselectableTappableTextView: UITextView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Native UITextView links gesture recognizers are broken on iOS 11.0-11.1:
// /questions/725485/xcode-9-uitextview-links-no-longer-clickable
// So we add our own UITapGestureRecognizer.
linkGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(textTapped))
linkGestureRecognizer.numberOfTapsRequired = 1
addGestureRecognizer(linkGestureRecognizer)
linkGestureRecognizer.isEnabled = true
}
var linkGestureRecognizer: UITapGestureRecognizer!
// required to prevent blue background selection from any situation
override var selectedTextRange: UITextRange? {
get { return nil }
set {}
}
override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
// Prevents drag and drop gestures,
// but also prevents a crash with links on iOS 11.0 and 11.1.
// https://stackoverflow.com/a/49535011/1033581
gestureRecognizer.isEnabled = false
super.addGestureRecognizer(gestureRecognizer)
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == linkGestureRecognizer {
// Supporting links correctly.
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
if gestureRecognizer is UIPanGestureRecognizer {
// Compatibility support with isScrollEnabled.
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
// Preventing selection gestures and disabling broken links support.
gestureRecognizer.isEnabled = false
return false
}
@objc func textTapped(recognizer: UITapGestureRecognizer) {
guard recognizer == linkGestureRecognizer else {
return
}
var location = recognizer.location(in: self)
location.x -= textContainerInset.left
location.y -= textContainerInset.top
let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let characterRange = NSRange(location: characterIndex, length: 1)
if let attachment = attributedText?.attribute(.attachment, at: index, effectiveRange: nil) as? NSTextAttachment {
if #available(iOS 10.0, *) {
_ = delegate?.textView?(self, shouldInteractWith: attachment, in: characterRange, interaction: .invokeDefaultAction)
} else {
_ = delegate?.textView?(self, shouldInteractWith: attachment, in: characterRange)
}
}
if let url = attributedText?.attribute(.link, at: index, effectiveRange: nil) as? URL {
if #available(iOS 10.0, *) {
_ = delegate?.textView?(self, shouldInteractWith: url, in: characterRange, interaction: .invokeDefaultAction)
} else {
_ = delegate?.textView?(self, shouldInteractWith: url, in: characterRange)
}
}
}
}
Ответ 5
Вот версия ответа от Objective C, которую написал Макс Чукимия.
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
UITextPosition *position = [self closestPositionToPoint:point];
if (!position) {
return NO;
}
UITextRange *range = [self.tokenizer rangeEnclosingPosition:position
withGranularity:UITextGranularityCharacter
inDirection:UITextLayoutDirectionLeft];
if (!range) {
return NO;
}
NSInteger startIndex = [self offsetFromPosition:self.beginningOfDocument
toPosition:range.start];
return [self.attributedText attribute:NSLinkAttributeName
atIndex:startIndex
effectiveRange:nil] != nil;
}
Ответ 6
Swift 3.0
Для выше Objective-C Версия через @Lukas
extension UITextView {
override open func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.isKind(of: UILongPressGestureRecognizer.self) {
do {
let array = try gestureRecognizer.value(forKey: "_targets") as! NSMutableArray
let targetAndAction = array.firstObject
let actions = ["action=oneFingerForcePan:",
"action=_handleRevealGesture:",
"action=loupeGesture:",
"action=longDelayRecognizer:"]
for action in actions {
print("targetAndAction.debugDescription: \(targetAndAction.debugDescription)")
if targetAndAction.debugDescription.contains(action) {
gestureRecognizer.isEnabled = false
}
}
} catch let exception {
print("TXT_VIEW EXCEPTION : \(exception)")
}
defer {
super.addGestureRecognizer(gestureRecognizer)
}
}
}
}
Ответ 7
Обновление для iOS 11, бета 6. Это самое простое и наиболее надежное в будущем решение, которое я нашел до сих пор, за исключением того, что мы катим наш собственный UITextView
с помощью TextKit
.
Swift 4
Часть 1:
public class ReadOnlyUITextView: UITextView {
fileprivate lazy var transparentCoveringView = UIView()
override public func updateConstraints() {
if transparentCoveringView.constraints.isEmpty {
addSubview(transparentCoveringView)
bringSubview(toFront: transparentCoveringView)
transparentCoveringView.backgroundColor = UIColor.clear
// Helper method.
transparentCoveringView.addFillViewConstraints(in: self)
}
super.updateConstraints()
}
/**
Override `becomeFirstResponder()`and return false to disable double-tap selection of links.
*/
override public func becomeFirstResponder() -> Bool {
return false
}
}
Часть 2:
Если вам нужно добавить экземпляр UILongPressGestureRecognizer
в экземпляр ReadOnlyUITextView
, как в моем случае, установить его атрибут minimumPressDuration
менее 0,325, поэтому он срабатывает до того, как будет определена вся система. распознования.
Что все:)
Ответ 8
Это работает для меня:
@interface MessageTextView : UITextView <UITextViewDelegate>
@end
@implementation MessageTextView
-(void)awakeFromNib{
[super awakeFromNib];
self.delegate = self;
}
- (BOOL)canBecomeFirstResponder {
return NO;
}
- (void)textViewDidChangeSelection:(UITextView *)textView
{
textView.selectedTextRange = nil;
[textView endEditing:YES];
}
@end
Ответ 9
Я закончил тем, что объединил решения из fooobar.com/questions/556919/... и fooobar.com/questions/556919/... (вариант iOS <11). Это работает, как и ожидалось: доступный только для чтения UITextView без выбора, над которым гиперссылки все еще работают. Одним из преимуществ решения Coeur является то, что обнаружение касания происходит мгновенно и не отображает выделение и не позволяет перетаскивать ссылку.
Вот результирующий код:
class HyperlinkEnabledReadOnlyTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
isEditable = false
isSelectable = false
initHyperLinkDetection()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isEditable = false
isSelectable = false
initHyperLinkDetection()
}
// MARK: - Prevent interaction except on hyperlinks
// Combining /questions/556919/uitextview-disable-selection-allow-links/2283060#2283060 and https://stackoverflow.com/a/49443814/1033581
private var linkGestureRecognizer: UITapGestureRecognizer!
private func initHyperLinkDetection() {
// Native UITextView links gesture recognizers are broken on iOS 11.0-11.1:
// https://stackoverflow.com/questions/46143868/xcode-9-uitextview-links-no-longer-clickable
// So we add our own UITapGestureRecognizer, which moreover detects taps faster than native one
linkGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(textTapped))
linkGestureRecognizer.numberOfTapsRequired = 1
addGestureRecognizer(linkGestureRecognizer)
linkGestureRecognizer.isEnabled = true // because previous call sets it to false
}
override func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
// Prevents drag and drop gestures, but also prevents a crash with links on iOS 11.0 and 11.1.
// https://stackoverflow.com/a/49535011/1033581
gestureRecognizer.isEnabled = false
super.addGestureRecognizer(gestureRecognizer)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// Allow only taps located over an hyperlink
var location = point
location.x -= textContainerInset.left
location.y -= textContainerInset.top
guard location.x >= bounds.minX, location.x <= bounds.maxX, location.y >= bounds.minY, location.y <= bounds.maxY else { return false }
let charIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return attributedText.attribute(.link, at: charIndex, effectiveRange: nil) != nil
}
@objc private func textTapped(recognizer: UITapGestureRecognizer) {
guard recognizer == linkGestureRecognizer else { return }
var location = recognizer.location(in: self)
location.x -= textContainerInset.left
location.y -= textContainerInset.top
guard location.x >= bounds.minX, location.x <= bounds.maxX, location.y >= bounds.minY, location.y <= bounds.maxY else { return }
let characterIndex = layoutManager.characterIndex(for: location, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let characterRange = NSRange(location: characterIndex, length: 1)
if let attachment = attributedText?.attribute(.attachment, at: index, effectiveRange: nil) as? NSTextAttachment {
if #available(iOS 10.0, *) {
_ = delegate?.textView?(self, shouldInteractWith: attachment, in: characterRange, interaction: .invokeDefaultAction)
} else {
_ = delegate?.textView?(self, shouldInteractWith: attachment, in: characterRange)
}
}
if let url = attributedText?.attribute(.link, at: characterIndex, effectiveRange: nil) as? URL {
if #available(iOS 10.0, *) {
_ = delegate?.textView?(self, shouldInteractWith: url, in: characterRange, interaction: .invokeDefaultAction)
} else {
_ = delegate?.textView?(self, shouldInteractWith: url, in: characterRange)
}
}
}
}
Пожалуйста, обратите внимание, что у меня возникли проблемы с компиляцией регистрационного .attachment
, я удалил его, потому что я им не пользуюсь.
Ответ 10
Перекрывайте UITextView, как показано ниже, и используйте его для визуализации сшиваемой ссылки с сохранением стиля HTML.
открытый класс LinkTextView: UITextView {
override public var selectedTextRange: UITextRange? {
get {
return nil
}
set {}
}
public init() {
super.init(frame: CGRect.zero, textContainer: nil)
commonInit()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
self.tintColor = UIColor.black
self.isScrollEnabled = false
self.delegate = self
self.dataDetectorTypes = []
self.isEditable = false
self.delegate = self
self.font = Style.font(.sansSerif11)
self.delaysContentTouches = true
}
@available(iOS 10.0, *)
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
// Handle link
return false
}
public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
// Handle link
return false
}
}
Ответ 11
Swift 4, Xcode 9.2
Ниже приведен другой подход для ссылки, сделайте свойство isSelectable
объекта UITextView равным false
class TextView: UITextView {
//MARK: Properties
open var didTouchedLink:((URL,NSRange,CGPoint) -> Void)?
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
super.draw(rect)
}
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = Array(touches)[0]
if let view = touch.view {
let point = touch.location(in: view)
self.tapped(on: point)
}
}
}
extension TextView {
fileprivate func tapped(on point:CGPoint) {
var location: CGPoint = point
location.x -= self.textContainerInset.left
location.y -= self.textContainerInset.top
let charIndex = layoutManager.characterIndex(for: location, in: self.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard charIndex < self.textStorage.length else {
return
}
var range = NSRange(location: 0, length: 0)
if let attributedText = self.attributedText {
if let link = attributedText.attribute(NSAttributedStringKey.link, at: charIndex, effectiveRange: &range) as? URL {
print("\n\t##-->You just tapped on '\(link)' withRange = \(NSStringFromRange(range))\n")
self.didTouchedLink?(link, range, location)
}
}
}
}
КАК ПОЛЬЗОВАТЬСЯ,
let textView = TextView()//Init your textview and assign attributedString and other properties you want.
textView.didTouchedLink = { (url,tapRange,point) in
//here goes your other logic for successfull URL location
}
Ответ 12
Вот как я решил эту проблему problem-. Я делаю свое выбираемое текстовое представление подклассом, который переопределяет canPerformAction и возвращает false.
class CustomTextView: UITextView {
override public func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return false
}
}
Ответ 13
Что я делаю для Objective C, так это создаю подкласс и перезаписываю textViewdidChangeSelection: метод делегата, поэтому в классе реализации:
#import "CustomTextView.h"
@interface CustomTextView()<UITextViewDelegate>
@end
@implementation CustomTextView
, , , , , ,
- (void) textViewDidChangeSelection:(UITextView *)textView
{
UITextRange *selectedRange = [textView selectedTextRange];
NSString *selectedText = [textView textInRange:selectedRange];
if (selectedText.length > 1 && selectedText.length < textView.text.length)
{
textView.selectedRange = NSMakeRange(0, 0);
}
}
Не забудьте установить self.delegate = self
Ответ 14
Здесь решение Swift 4, которое позволяет касаниям проходить через желоб, кроме случаев, когда нажата ссылка;
В родительском представлении
private(set) lazy var textView = YourCustomTextView()
func setupView() {
textView.isScrollEnabled = false
textView.isUserInteractionEnabled = false
let tapGr = UITapGestureRecognizer(target: textView, action: nil)
tapGr.delegate = textView
addGestureRecognizer(tapGr)
textView.translatesAutoresizingMaskIntoConstraints = false
addSubview(textView)
NSLayoutConstraint.activate(textView.edges(to: self))
}
Пользовательский UITextView
class YourCustomTextView: UITextView, UIGestureRecognizerDelegate {
var onLinkTapped: (URL) -> Void = { print($0) }
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let gesture = gestureRecognizer as? UITapGestureRecognizer else {
return true
}
let location = gesture.location(in: self)
guard let closest = closestPosition(to: location), let startPosition = position(from: closest, offset: -1), let endPosition = position(from: closest, offset: 1) else {
return false
}
guard let textRange = textRange(from: startPosition, to: endPosition) else {
return false
}
let startOffset = offset(from: beginningOfDocument, to: textRange.start)
let endOffset = offset(from: beginningOfDocument, to: textRange.end)
let range = NSRange(location: startOffset, length: endOffset - startOffset)
guard range.location != NSNotFound, range.length != 0 else {
return false
}
guard let linkAttribute = attributedText.attributedSubstring(from: range).attribute(.link, at: 0, effectiveRange: nil) else {
return false
}
guard let linkString = linkAttribute as? String, let url = URL(string: linkString) else {
return false
}
guard delegate?.textView?(self, shouldInteractWith: url, in: range, interaction: .invokeDefaultAction) ?? true else {
return false
}
onLinkTapped(url)
return true
}
}
Ответ 15
Swift 4.2
просто
class MyTextView: UITextView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
guard let pos = closestPosition(to: point) else { return false }
guard let range = tokenizer.rangeEnclosingPosition(pos, with: .character, inDirection: UITextDirection(rawValue: UITextLayoutDirection.left.rawValue)) else { return false }
let startIndex = offset(from: beginningOfDocument, to: range.start)
return attributedText.attribute(NSAttributedString.Key.link, at: startIndex, effectiveRange: nil) != nil
}
}
Ответ 16
Уродливый, но вкусный.
private class LinkTextView: UITextView {
override func selectionRects(for range: UITextRange) -> [UITextSelectionRect] {
[]
}
override func caretRect(for position: UITextPosition) -> CGRect {
CGRect.zero.offsetBy(dx: .greatestFiniteMagnitude, dy: .greatestFiniteMagnitude)
}
}
Протестировано с текстовым представлением, где прокрутка была отключена.