Обнаружение ответвлений от атрибута текста в UITextView в iOS
У меня есть UITextView
который отображает NSAttributedString
. Эта строка содержит слова, которые я хотел бы сделать так, чтобы их можно было использовать так, чтобы при их нажатии меня перезванивали, чтобы я мог выполнить действие. Я понимаю, что UITextView
может обнаружить нажатия на URL-адрес и перезвонить моему делегату, но это не URL-адреса.
Мне кажется, что с iOS 7 и мощью TextKit это теперь возможно, однако я не могу найти никаких примеров и не знаю, с чего начать.
Я понимаю, что теперь можно создавать пользовательские атрибуты в строке (хотя я еще не сделал этого), и, возможно, они будут полезны для обнаружения, если одно из волшебных слов было нажато? В любом случае, я до сих пор не знаю, как перехватить это нажатие и определить, по какому слову произошло нажатие.
Обратите внимание, что совместимость с iOS 6 не требуется.
Ответы
Ответ 1
Я просто хотел помочь другим немного больше. Следуя от ответа Шмидта, можно сделать то, что я задал в своем первоначальном вопросе.
1) Создайте атрибутированную строку с настраиваемыми атрибутами, применяемыми к словам, которые можно щелкнуть. например.
NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:@"a clickable word" attributes:@{ @"myCustomTag" : @(YES) }];
[paragraph appendAttributedString:attributedString];
2) Создайте UITextView, чтобы отобразить эту строку, и добавьте к ней UITapGestureRecognizer. Затем обработайте кран:
- (void)textTapped:(UITapGestureRecognizer *)recognizer
{
UITextView *textView = (UITextView *)recognizer.view;
// Location of the tap in text-container coordinates
NSLayoutManager *layoutManager = textView.layoutManager;
CGPoint location = [recognizer locationInView:textView];
location.x -= textView.textContainerInset.left;
location.y -= textView.textContainerInset.top;
// Find the character that been tapped on
NSUInteger characterIndex;
characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textView.textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
if (characterIndex < textView.textStorage.length) {
NSRange range;
id value = [textView.attributedText attribute:@"myCustomTag" atIndex:characterIndex effectiveRange:&range];
// Handle as required...
NSLog(@"%@, %d, %d", value, range.location, range.length);
}
}
Так просто, когда вы знаете, как!
Ответ 2
Обнаружение нажатий на атрибутивный текст с помощью Swift
Иногда для начинающих немного сложно понять, как все настроить (это было в любом случае для меня), поэтому этот пример немного полнее.
Добавьте UITextView
в ваш проект.
![6YTS4.png]()
Выход
Подключите UITextView
к ViewController
с выходом с именем textView
.
Пользовательский атрибут
Мы собираемся сделать собственный атрибут, сделав Расширение.
Примечание: Этот шаг технически необязателен, но если вы этого не сделаете, вам нужно будет отредактировать код в следующей части, чтобы использовать стандартный атрибут, такой как NSAttributedString.Key.foregroundColor
. Преимущество использования пользовательского атрибута состоит в том, что вы можете определить, какие значения вы хотите сохранить в указанном текстовом диапазоне.
Добавьте новый файл swift с помощью меню "Файл"> "Создать"> "Файл"> "iOS"> "Источник"> "Файл Swift". Вы можете называть это как хотите. Я называю мой NSAttributedStringKey + CustomAttribute.swift.
Вставьте следующий код:
import Foundation
extension NSAttributedString.Key {
static let myAttributeName = NSAttributedString.Key(rawValue: "MyCustomAttribute")
}
Код
Замените код в ViewController.swift следующим. Обратите внимание на UIGestureRecognizerDelegate
.
import UIKit
class ViewController: UIViewController, UIGestureRecognizerDelegate {
@IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Create an attributed string
let myString = NSMutableAttributedString(string: "Swift attributed text")
// Set an attribute on part of the string
let myRange = NSRange(location: 0, length: 5) // range of "Swift"
let myCustomAttribute = [ NSAttributedString.Key.myAttributeName: "some value"]
myString.addAttributes(myCustomAttribute, range: myRange)
textView.attributedText = myString
// Add tap gesture recognizer to Text View
let tap = UITapGestureRecognizer(target: self, action: #selector(myMethodToHandleTap(_:)))
tap.delegate = self
textView.addGestureRecognizer(tap)
}
@objc func myMethodToHandleTap(_ sender: UITapGestureRecognizer) {
let myTextView = sender.view as! UITextView
let layoutManager = myTextView.layoutManager
// location of tap in myTextView coordinates and taking the inset into account
var location = sender.location(in: myTextView)
location.x -= myTextView.textContainerInset.left;
location.y -= myTextView.textContainerInset.top;
// character index at tap location
let characterIndex = layoutManager.characterIndex(for: location, in: myTextView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// if index is valid then do something.
if characterIndex < myTextView.textStorage.length {
// print the character index
print("character index: \(characterIndex)")
// print the character at the index
let myRange = NSRange(location: characterIndex, length: 1)
let substring = (myTextView.attributedText.string as NSString).substring(with: myRange)
print("character at index: \(substring)")
// check if the tap location has a certain attribute
let attributeName = NSAttributedString.Key.myAttributeName
let attributeValue = myTextView.attributedText?.attribute(attributeName, at: characterIndex, effectiveRange: nil)
if let value = attributeValue {
print("You tapped on \(attributeName.rawValue) and the value is: \(value)")
}
}
}
}
![enter image description here]()
Теперь, если вы нажмете "w" в "Swift", вы должны получить следующий результат:
character index: 1
character at index: w
You tapped on MyCustomAttribute and the value is: some value
Заметки
- Здесь я использовал пользовательский атрибут, но он также мог быть
NSAttributedString.Key.foregroundColor
(цвет текста) со значением UIColor.green
. - Раньше текстовое представление не могло быть редактируемым или выбираемым, но в моем обновленном ответе для Swift 4.2 оно, кажется, работает нормально, независимо от того, выбраны они или нет.
Дальнейшее обучение
Этот ответ был основан на нескольких других ответах на этот вопрос. Помимо этого, см. Также
Ответ 3
Это немного измененная версия, построенная на ответе @tarmes. Я не мог получить переменную value
для возврата ничего, кроме null
без настройки ниже. Кроме того, мне нужен полный словарь атрибутов, возвращаемый для определения полученного результата. Я бы поместил это в комментарии, но, похоже, не имел репутации для этого. Извините заранее, если я нарушил протокол.
Конкретная настройка - использовать textView.textStorage
вместо textView.attributedText
. Будучи еще программистом iOS, я не совсем уверен, почему это так, но, возможно, кто-то еще может нас просветить.
Конкретная модификация метода обработки крана:
NSDictionary *attributesOfTappedText = [textView.textStorage attributesAtIndex:characterIndex effectiveRange:&range];
Полный код в моем контроллере
- (void)viewDidLoad
{
[super viewDidLoad];
self.textView.attributedText = [self attributedTextViewString];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(textTapped:)];
[self.textView addGestureRecognizer:tap];
}
- (NSAttributedString *)attributedTextViewString
{
NSMutableAttributedString *paragraph = [[NSMutableAttributedString alloc] initWithString:@"This is a string with " attributes:@{NSForegroundColorAttributeName:[UIColor blueColor]}];
NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:@"a tappable string"
attributes:@{@"tappable":@(YES),
@"networkCallRequired": @(YES),
@"loadCatPicture": @(NO)}];
NSAttributedString* anotherAttributedString = [[NSAttributedString alloc] initWithString:@" and another tappable string"
attributes:@{@"tappable":@(YES),
@"networkCallRequired": @(NO),
@"loadCatPicture": @(YES)}];
[paragraph appendAttributedString:attributedString];
[paragraph appendAttributedString:anotherAttributedString];
return [paragraph copy];
}
- (void)textTapped:(UITapGestureRecognizer *)recognizer
{
UITextView *textView = (UITextView *)recognizer.view;
// Location of the tap in text-container coordinates
NSLayoutManager *layoutManager = textView.layoutManager;
CGPoint location = [recognizer locationInView:textView];
location.x -= textView.textContainerInset.left;
location.y -= textView.textContainerInset.top;
NSLog(@"location: %@", NSStringFromCGPoint(location));
// Find the character that been tapped on
NSUInteger characterIndex;
characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textView.textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
if (characterIndex < textView.textStorage.length) {
NSRange range;
NSDictionary *attributes = [textView.textStorage attributesAtIndex:characterIndex effectiveRange:&range];
NSLog(@"%@, %@", attributes, NSStringFromRange(range));
//Based on the attributes, do something
///if ([attributes objectForKey:...)] //make a network call, load a cat Pic, etc
}
}
Ответ 4
Создание специальной ссылки и выполнение того, что вы хотите на кране, стало намного проще с iOS 7.
Очень хороший пример в Ray Wenderlich
Ответ 5
Пример WWDC 2013:
NSLayoutManager *layoutManager = textView.layoutManager;
CGPoint location = [touch locationInView:textView];
NSUInteger characterIndex;
characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textView.textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
if (characterIndex < textView.textStorage.length) {
// valid index
// Find the word range here
// using -enumerateSubstringsInRange:options:usingBlock:
}
Ответ 6
Мне удалось решить эту проблему просто с помощью NSLinkAttributeName
Swift 2
class MyClass: UIViewController, UITextViewDelegate {
@IBOutlet weak var tvBottom: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let attributedString = NSMutableAttributedString(string: "click me ok?")
attributedString.addAttribute(NSLinkAttributeName, value: "cs://moreinfo", range: NSMakeRange(0, 5))
tvBottom.attributedText = attributedString
tvBottom.delegate = self
}
func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
UtilityFunctions.alert("clicked", message: "clicked")
return false
}
}
Ответ 7
Полный пример для обнаружения действий приписываемого текста с помощью Swift 3
let termsAndConditionsURL = TERMS_CONDITIONS_URL;
let privacyURL = PRIVACY_URL;
override func viewDidLoad() {
super.viewDidLoad()
self.txtView.delegate = self
let str = "By continuing, you accept the Terms of use and Privacy policy"
let attributedString = NSMutableAttributedString(string: str)
var foundRange = attributedString.mutableString.range(of: "Terms of use") //mention the parts of the attributed text you want to tap and get an custom action
attributedString.addAttribute(NSLinkAttributeName, value: termsAndConditionsURL, range: foundRange)
foundRange = attributedString.mutableString.range(of: "Privacy policy")
attributedString.addAttribute(NSLinkAttributeName, value: privacyURL, range: foundRange)
txtView.attributedText = attributedString
}
И затем вы можете поймать действие с помощью метода shouldInteractWith URL
UITextViewDelegate делегирования. Убедитесь, что вы правильно установили делегат.
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "WebView") as! SKWebViewController
if (URL.absoluteString == termsAndConditionsURL) {
vc.strWebURL = TERMS_CONDITIONS_URL
self.navigationController?.pushViewController(vc, animated: true)
} else if (URL.absoluteString == privacyURL) {
vc.strWebURL = PRIVACY_URL
self.navigationController?.pushViewController(vc, animated: true)
}
return false
}
Как и мудрый, вы можете выполнить любое действие согласно вашему требованию.
Ура!!
Ответ 8
Это можно сделать с помощью characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints:
. Он будет работать несколько иначе, чем вы хотели - вам нужно будет проверить, принадлежит ли затронутый персонаж волшебному слову. Но это не должно быть сложно.
BTW Я настоятельно рекомендую смотреть Введение в текстовый набор из WWDC 2013.
Ответ 9
Это может работать нормально с короткой ссылкой, многоканальной в текстовом виде. Он работает нормально с iOS 6,7,8.
- (void)tappedTextView:(UITapGestureRecognizer *)tapGesture {
if (tapGesture.state != UIGestureRecognizerStateEnded) {
return;
}
UITextView *textView = (UITextView *)tapGesture.view;
CGPoint tapLocation = [tapGesture locationInView:textView];
NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink|NSTextCheckingTypePhoneNumber
error:nil];
NSArray* resultString = [detector matchesInString:self.txtMessage.text options:NSMatchingReportProgress range:NSMakeRange(0, [self.txtMessage.text length])];
BOOL isContainLink = resultString.count > 0;
if (isContainLink) {
for (NSTextCheckingResult* result in resultString) {
CGRect linkPosition = [self frameOfTextRange:result.range inTextView:self.txtMessage];
if(CGRectContainsPoint(linkPosition, tapLocation) == 1){
if (result.resultType == NSTextCheckingTypePhoneNumber) {
NSString *phoneNumber = [@"telprompt://" stringByAppendingString:result.phoneNumber];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:phoneNumber]];
}
else if (result.resultType == NSTextCheckingTypeLink) {
[[UIApplication sharedApplication] openURL:result.URL];
}
}
}
}
}
- (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView
{
UITextPosition *beginning = textView.beginningOfDocument;
UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
UITextPosition *end = [textView positionFromPosition:start offset:range.length];
UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
CGRect firstRect = [textView firstRectForRange:textRange];
CGRect newRect = [textView convertRect:firstRect fromView:textView.textInputView];
return newRect;
}
Ответ 10
В Swift 5 и iOS 12 вы можете создать подкласс UITextView
и переопределить point(inside:with:)
с некоторой реализацией TextKit, чтобы сделать только некоторые NSAttributedStrings
в нем доступными.
В следующем коде показано, как создать UITextView
который реагирует только на нажатия на подчеркнутую NSAttributedString
в нем:
InteractiveUnderlinedTextView.swift
import UIKit
class InteractiveUnderlinedTextView: UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
func configure() {
isScrollEnabled = false
isEditable = false
isSelectable = false
isUserInteractionEnabled = true
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let superBool = super.point(inside: point, with: event)
let characterIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard characterIndex < textStorage.length else { return false }
let attributes = textStorage.attributes(at: characterIndex, effectiveRange: nil)
return superBool && attributes[NSAttributedString.Key.underlineStyle] != nil
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let linkTextView = InteractiveUnderlinedTextView()
linkTextView.backgroundColor = .orange
let mutableAttributedString = NSMutableAttributedString(string: "Some text\n\n")
let attributes = [NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue]
let underlinedAttributedString = NSAttributedString(string: "Some other text", attributes: attributes)
mutableAttributedString.append(underlinedAttributedString)
linkTextView.attributedText = mutableAttributedString
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(underlinedTextTapped))
linkTextView.addGestureRecognizer(tapGesture)
view.addSubview(linkTextView)
linkTextView.translatesAutoresizingMaskIntoConstraints = false
linkTextView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
linkTextView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
linkTextView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor).isActive = true
}
@objc func underlinedTextTapped(_ sender: UITapGestureRecognizer) {
print("Hello")
}
}
Ответ 11
Используйте это расширение для Swift:
import UIKit
extension UITapGestureRecognizer {
func didTapAttributedTextInTextView(textView: UITextView, inRange targetRange: NSRange) -> Bool {
let layoutManager = textView.layoutManager
let locationOfTouch = self.location(in: textView)
let index = layoutManager.characterIndex(for: locationOfTouch, in: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return NSLocationInRange(index, targetRange)
}
}
Добавьте UITapGestureRecognizer
в текстовое представление с помощью следующего селектора:
guard let text = textView.attributedText?.string else {
return
}
let textToTap = "Tap me"
if let range = text.range(of: tapableText),
tapGesture.didTapAttributedTextInTextView(textView: textTextView, inRange: NSRange(range, in: text)) {
// Tap recognized
}