web-dev-qa-db-ja.com

UITextView:選択を無効にし、リンクを許可します

UITextView があり、NSAttributedStringが表示されます。 textViewのeditableプロパティとselectableプロパティは両方ともfalseに設定されます。

AttributedStringにはURLが含まれており、URLをタップしてブラウザーを開くことができるようにします。ただし、URLとの対話は、selectable属性がtrueに設定されている場合にのみ可能です。

リンクのタップに対してのみユーザー操作を許可し、テキストの選択に対しては許可しないようにするにはどうすればよいですか?

30
130e13a

私は内部のジェスチャー認識機能をいじるという概念が少し怖いので、別の解決策を見つけようとしました。 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:)が呼び出されることを意味します:)

66
Max Chuquimia

Cœurが言ったように、UITextViewのメソッドをオーバーライドするselectedTextRangeをサブクラス化して、nilに設定できます。また、リンクは引き続きクリック可能ですが、テキストの残りの部分を選択することはできません。

class CustomTextView: UITextView {
override public var selectedTextRange: UITextRange? {
    get {
        return nil
    }
    set { }
}
9

そのため、いくつかの調査の後、解決策を見つけることができました。これはハックであり、今後のiOSバージョンで機能するかどうかはわかりませんが、現時点(iOS 9.3)で機能します。

このUITextViewカテゴリーを追加するだけです(要点 ここ ):

@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];
        }
    }
}
5
130e13a

最小展開ターゲットがiOS 11.2以降の場合

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
        // https://stackoverflow.com/questions/46143868/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で壊れており、tapではなくsmall delay long pressが必要です:- Xcode 9 UITextViewリンクはクリック不可

独自のジェスチャレコグナイザーとのリンクを適切にサポートできます。また、UITextViewをサブクラス化し、何かを選択またはタップできるジェスチャーを禁止することで、テキスト選択を無効にできます。

以下の解決策は選択を許可せず、次のとおりです。

  • isScrollEnabledとの互換性
  • リンクとの互換性
  • iOS 11.0およびiOS 11.1の回避策の制限ですが、テキストの添付ファイルをタップするとUI効果が失われます
/// 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:
        // https://stackoverflow.com/questions/46143868/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)
            }
        }
    }
}
3
Cœur

Max Chuquimiaが投稿した回答の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;
}
2
Marcin Kuptel

https://stackoverflow.com/a/44878203/2015332https://stackoverflow.com/a/49443814/2015332 (iOS < 11バリアント)。これは期待どおりに機能します。ハイパーリンクがまだ機能している読み取り専用の選択不可能なUITextViewです。 Coeurのソリューションの利点の1つは、タッチ検出が即座に行われ、ハイライトが表示されず、リンクのドラッグアンドドロップができないことです。

結果のコードは次のとおりです。

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 https://stackoverflow.com/a/44878203/2015332 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 enumの場合、使用していないため削除しました。

1
fred

これは私のために働く:

@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
1
jegorenkov

Swift 3.0

@Lukasを介した上記のObjective-Cバージョン

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)
                }
            }
        }

    }
1

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
    }
}
0
Oscar Apeland

この問題を解決する方法を次に示します。選択可能なテキストビューを、canPerformActionをオーバーライドしてfalseを返すサブクラスにします。

class CustomTextView: UITextView {

override public func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        return false
    }

}
0
Phillip English

Swift 4、Xcode 9.2

以下はリンクの別のアプローチです。UITextViewのisSelectableプロパティを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
}
0
Vatsal Shukla

以下のように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
}

}

0
deepax11

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
    }
}
0
Ahmed Safadi

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を設定することを忘れないでください

0
JFCa