J'ai un UITextView
qui affiche un NSAttributedString
. Cette chaîne contient des mots que je voudrais rendre susceptibles d’être effacés, de sorte que lorsqu’ils sont tapés, je suis rappelé afin que je puisse effectuer une action. Je me rends compte que UITextView
peut détecter des taps sur une URL et rappeler mon délégué, mais ce ne sont pas des URL.
Il me semble qu'avec iOS 7 et la puissance de TextKit, cela devrait maintenant être possible, mais je ne trouve aucun exemple et je ne sais pas par où commencer.
Je comprends qu’il est maintenant possible de créer des attributs personnalisés dans la chaîne (bien que je ne l’aie pas encore fait) et que ceux-ci seront utiles pour détecter si l’un des mots magiques a été exploité. Dans tous les cas, je ne sais toujours pas comment intercepter ce tapotement et détecter sur quel mot le tapotement a eu lieu.
Notez que la compatibilité iOS 6 est non requise.
Je voulais juste aider les autres un peu plus. Suite à la réponse de Shmidt, il est possible de faire exactement ce que j'avais demandé dans ma question initiale.
1) Créez une chaîne attribuée avec des attributs personnalisés appliqués aux mots cliquables. par exemple.
NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:@"a clickable Word" attributes:@{ @"myCustomTag" : @(YES) }];
[paragraph appendAttributedString:attributedString];
2) Créez un UITextView pour afficher cette chaîne et ajoutez-lui un UITapGestureRecognizer. Puis manipulez le robinet:
- (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's 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);
}
}
Tellement facile quand vous savez comment!
Parfois, pour les débutants, il est un peu difficile de savoir comment faire pour mettre les choses en place (ce fut pour moi de toute façon), alors cet exemple est un peu plus complet.
UITextView
à votre projet.Connectez le UITextView
au ViewController
avec une sortie nommée textView
.
Nous allons créer un attribut personnalisé en faisant un Extension .
Remarque: Cette étape est techniquement optionnelle, mais si vous ne le faites pas, vous devrez éditer le code dans la partie suivante pour utiliser un attribut standard. comme
NSAttributedString.Key.foregroundColor
. L'utilisation d'un attribut personnalisé présente l'avantage de pouvoir définir les valeurs que vous souhaitez stocker dans la plage de texte attribuée.
Ajouter un nouveau fichier Swift avec Fichier> Nouveau> Fichier ...> iOS> Source> Swift Fichier . Vous pouvez appeler cela comme vous voulez. J'appelle le mien NSAttributedStringKey + CustomAttribute.Swift.
Collez dans le code suivant:
import Foundation
extension NSAttributedString.Key {
static let myAttributeName = NSAttributedString.Key(rawValue: "MyCustomAttribute")
}
Remplacez le code dans ViewController.Swift par le suivant. Notez le 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)")
}
}
}
}
Maintenant, si vous appuyez sur le "w" de "Swift", vous devriez obtenir le résultat suivant:
character index: 1
character at index: w
You tapped on MyCustomAttribute and the value is: some value
NSAttributedString.Key.foregroundColor
(couleur du texte) ayant la valeur UIColor.green
.Cette réponse reposait sur plusieurs autres réponses à cette question. Outre ceux-ci, voir aussi
Ceci est une version légèrement modifiée, basée sur la réponse de @tarmes. Je ne pouvais pas obtenir la variable value
rapporter autre chose que null
sans le Tweak ci-dessous. En outre, j'avais besoin du dictionnaire d'attributs complet renvoyé afin de déterminer l'action résultante. J'aurais mis cela dans les commentaires mais ne semble pas avoir le représentant pour le faire. Excuse-moi d'avance si j'ai violé le protocole.
Tweak spécifique est d'utiliser textView.textStorage
au lieu de textView.attributedText
. En tant que programmeur iOS en apprentissage, je ne sais pas trop pourquoi, mais quelqu'un d'autre peut peut-être nous éclairer.
Modification spécifique dans la méthode de traitement du robinet:
NSDictionary *attributesOfTappedText = [textView.textStorage attributesAtIndex:characterIndex effectiveRange:&range];
Code complet dans mon contrôleur de vue
- (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's 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
}
}
Faire un lien personnalisé et faire ce que vous voulez au robinet est devenu beaucoup plus facile avec iOS 7. Il existe un très bon exemple sur Ray Wenderlich
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:
}
J'ai pu résoudre cela assez simplement avec 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
}
}
Exemple complet pour détecter des actions sur un texte attribué avec Swift
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
}
Et puis vous pouvez voir l'action avec shouldInteractWith URL
UITextViewDelegate delegate method.Assurez-vous d’avoir défini le délégué correctement.
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
}
De même, vous pouvez effectuer n'importe quelle action en fonction de vos besoins.
À votre santé!!
Il est possible de faire cela avec characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints:
. Cela fonctionnera un peu différemment de ce que vous vouliez - vous devrez tester si un personnage engagé appartient à un mot magique. Mais ça ne devrait pas être compliqué.
BTW, je recommande fortement de regarder Introducing Text Kit à partir de WWDC 2013.
Utilisez cette extension pour 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)
}
}
Ajoutez UITapGestureRecognizer
à votre affichage texte avec le sélecteur suivant:
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
}
Celui-ci pourrait fonctionner correctement avec un lien court, une liaison multiple dans une vue de texte. Cela fonctionne bien avec 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;
}
Avec Swift 5 et iOS 12, vous pouvez créer une sous-classe de UITextView
et le remplacer par point(inside:with:)
avec une implémentation TextKit dans l'ordre pour ne faire que quelques NSAttributedStrings
en tappable.
Le code suivant montre comment créer un UITextView
qui réagit uniquement en tapant sur le NSAttributedString
s souligné:
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")
}
}