¿Cómo puedo hacer un enlace cliqueable en NSAttributedString?

Es trivial hacer que los hipervínculos se puedan hacer clic en una UITextView . Simplemente establece la casilla “detectar enlaces” en la vista en IB, y detecta enlaces HTTP y los convierte en hipervínculos.

Sin embargo, eso todavía significa que lo que el usuario ve es el enlace “en bruto”. Los archivos RTF y HTML le permiten configurar una cadena legible por el usuario con un enlace “detrás”.

Es fácil instalar texto atribuido en una vista de texto (o un UILabel o UITextField , para el caso). Sin embargo, cuando ese texto atribuido incluye un enlace, no se puede hacer clic en él.

¿Hay alguna manera de hacer que el texto legible por el usuario se pueda hacer clic en un UITextView , UILabel o UITextField ?

El marcado es diferente en SO, pero aquí está la idea general. Lo que quiero es un texto como este:

Este cambio se generó con Face Dancer , haga clic para ver en la tienda de aplicaciones.

Lo único que puedo conseguir es esto:

Este cambio se generó con Face Dancer, haga clic en http://example.com/facedancer para verlo en la tienda de aplicaciones.

Use NSMutableAttributedString .

 NSMutableAttributedString * str = [[NSMutableAttributedString alloc] initWithString:@"Google"]; [str addAttribute: NSLinkAttributeName value: @"http://www.google.com" range: NSMakeRange(0, str.length)]; yourTextView.attributedText = str; 

Editar :

Esto no es directamente sobre la cuestión, sino solo para aclarar, UITextField y UILabel no admite la apertura de URL. Si desea usar UILabel con enlaces, puede verificar TTTAttributedLabel .

También debe establecer el valor de dataDetectorTypes de su UITextView en UIDataDetectorTypeLink o UIDataDetectorTypeAll para abrir las URL cuando se hace clic. O puede usar el método de delegado como se sugiere en los comentarios.

Encontré esto realmente útil, pero necesitaba hacerlo en bastantes lugares, así que he ajustado mi enfoque en una extensión simple a NSMutableAttributedString :

Swift 3

 extension NSMutableAttributedString { public func setAsLink(textToFind:String, linkURL:String) -> Bool { let foundRange = self.mutableString.range(of: textToFind) if foundRange.location != NSNotFound { self.addAttribute(.link, value: linkURL, range: foundRange) return true } return false } } 

Swift 2

 import Foundation extension NSMutableAttributedString { public func setAsLink(textToFind:String, linkURL:String) -> Bool { let foundRange = self.mutableString.rangeOfString(textToFind) if foundRange.location != NSNotFound { self.addAttribute(NSLinkAttributeName, value: linkURL, range: foundRange) return true } return false } } 

Ejemplo de uso:

 let attributedString = NSMutableAttributedString(string:"I love stackoverflow!") let linkWasSet = attributedString.setAsLink("stackoverflow", linkURL: "http://stackoverflow.com") if linkWasSet { // adjust more attributedString properties } 

C objective

Acabo de cumplir un requisito para hacer lo mismo en un proyecto puro de Objective-C, así que aquí está la categoría Objective-C.

 @interface NSMutableAttributedString (SetAsLinkSupport) - (BOOL)setAsLink:(NSString*)textToFind linkURL:(NSString*)linkURL; @end @implementation NSMutableAttributedString (SetAsLinkSupport) - (BOOL)setAsLink:(NSString*)textToFind linkURL:(NSString*)linkURL { NSRange foundRange = [self.mutableString rangeOfString:textToFind]; if (foundRange.location != NSNotFound) { [self addAttribute:NSLinkAttributeName value:linkURL range:foundRange]; return YES; } return NO; } @end 

Ejemplo de uso:

 NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:"I love stackoverflow!"]; BOOL linkWasSet = [attributedString setAsLink:@"stackoverflow" linkURL:@"http://stackoverflow.com"]; if (linkWasSet) { // adjust more attributedString properties } 

Mejora menor a la solución de ujell: si usa NSURL en lugar de NSString, puede usar cualquier URL (por ejemplo, URL personalizadas)

 NSURL *URL = [NSURL URLWithString: @"whatsapp://app"]; NSMutableAttributedString * str = [[NSMutableAttributedString alloc] initWithString:@"start Whatsapp"]; [str addAttribute: NSLinkAttributeName value:URL range: NSMakeRange(0, str.length)]; yourTextField.attributedText = str; 

¡Que te diviertas!

Acabo de crear una subclase de UILabel para abordar especialmente estos casos de uso. Puede agregar múltiples enlaces fácilmente y definir diferentes controladores para ellos. También es compatible con resaltar el enlace presionado cuando toca hacia abajo para obtener información táctil. Por favor, consulte https://github.com/null09264/FRHyperLabel .

En su caso, el código puede tener este aspecto:

 FRHyperLabel *label = [FRHyperLabel new]; NSString *string = @"This morph was generated with Face Dancer, Click to view in the app store."; NSDictionary *attributes = @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]}; label.attributedText = [[NSAttributedString alloc]initWithString:string attributes:attributes]; [label setLinkForSubstring:@"Face Dancer" withLinkHandler:^(FRHyperLabel *label, NSString *substring){ [[UIApplication sharedApplication] openURL:aURL]; }]; 

Captura de pantalla de muestra (el controlador está configurado para mostrar una alerta en lugar de abrir una url en este caso)

facedancer

Yo también tenía un requisito similar, inicialmente usé UILabel y luego me di cuenta de que UITextView es mejor. Hice que UITextView se comportara como UILabel al deshabilitar la interacción y el desplazamiento e hice un método de categoría para NSMutableAttributedString para establecer un enlace al texto igual a lo que había hecho Karl (+1 para eso) esta es mi versión obj c

 -(void)setTextAsLink:(NSString*) textToFind withLinkURL:(NSString*) url { NSRange range = [self.mutableString rangeOfString:textToFind options:NSCaseInsensitiveSearch]; if (range.location != NSNotFound) { [self addAttribute:NSLinkAttributeName value:url range:range]; [self addAttribute:NSForegroundColorAttributeName value:[UIColor URLColor] range:range]; } } 

puedes usar el siguiente delegado para manejar la acción

 - (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)url inRange:(NSRange)characterRange { // do the task return YES; } 

Use UITextView admite enlaces clicables. Crear cadena atribuida usando el siguiente código

 NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:strSomeTextWithLinks]; 

A continuación, configure texto UITextView de la siguiente manera

 NSDictionary *linkAttributes = @{NSForegroundColorAttributeName: [UIColor redColor], NSUnderlineColorAttributeName: [UIColor blueColor], NSUnderlineStyleAttributeName: @(NSUnderlinePatternSolid)}; customTextView.linkTextAttributes = linkAttributes; // customizes the appearance of links textView.attributedText = attributedString; 

Asegúrese de habilitar el comportamiento “seleccionable” de UITextView en XIB.

El meollo de mi pregunta fue que quería poder crear enlaces clicables en vistas de texto / campos / tags sin tener que escribir código personalizado para manipular el texto y agregar los enlaces. Yo quería que sea impulsado por los datos.

Finalmente descubrí cómo hacerlo. El problema es que IB no respeta los enlaces integrados.

Además, la versión iOS de NSAttributedString no le permite inicializar una cadena atribuida desde un archivo RTF. La versión OS X de NSAttributedString tiene un inicializador que toma un archivo RTF como entrada.

NSAttributedString ajusta al protocolo NSCoding, por lo que puede convertirlo a / desde NSData

Creé una herramienta de línea de comandos de OS X que toma un archivo RTF como entrada y saca un archivo con la extensión .data que contiene NSData de NSCoding. Luego coloco el archivo .data en mi proyecto y agrego un par de líneas de código que carga el texto en la vista. El código se ve así (este proyecto estaba en Swift):

 /* If we can load a file called "Dates.data" from the bundle and convert it to an attributed string, install it in the dates field. The contents contain clickable links with custom URLS to select each date. */ if let datesPath = NSBundle.mainBundle().pathForResource("Dates", ofType: "data"), let datesString = NSKeyedUnarchiver.unarchiveObjectWithFile(datesPath) as? NSAttributedString { datesField.attributedText = datesString } 

Para las aplicaciones que usan mucho texto formateado, creo una regla de comstackción que le dice a Xcode que todos los archivos .rtf en una carpeta dada son fuente y los archivos .data son el resultado. Una vez que hago eso, simplemente agrego los archivos .rtf al directorio designado (o edito los archivos existentes) y el proceso de comstackción se da cuenta de que son nuevos / actualizados, ejecuta la herramienta de línea de comandos y copia los archivos en el paquete de la aplicación. Funciona maravillosamente.

Escribí una publicación de blog que vincula a un proyecto de muestra (Swift) que demuestra la técnica. Puedes verlo aqui:

Creación de URL clicables en un UITextField que se abre en su aplicación

Ejemplo de Swift 3 para detectar acciones en toques de texto atribuidos

https://stackoverflow.com/a/44226491/5516830

 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 } 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 la misma manera, puedes agregar cualquier acción que desees con el método shouldInteractWith URL UITextFieldDelegate.

¡¡Aclamaciones!!

Swift 4:

 var string = "Google" var attributedString = NSMutableAttributedString(string: string, attributes:[NSAttributedStringKey.link: URL(string: "http://www.google.com")!]) yourTextView.attributedText = attributedString 

Swift 3.1:

 var string = "Google" var attributedString = NSMutableAttributedString(string: string, attributes:[NSLinkAttributeName: URL(string: "http://www.google.com")!]) yourTextView.attributedText = attributedString 

He escrito un método que agrega un enlace (linkString) a una cadena (fullString) con una determinada url (urlString):

 - (NSAttributedString *)linkedStringFromFullString:(NSString *)fullString withLinkString:(NSString *)linkString andUrlString:(NSString *)urlString { NSRange range = [fullString rangeOfString:linkString options:NSLiteralSearch]; NSMutableAttributedString *str = [[NSMutableAttributedString alloc] initWithString:fullString]; NSMutableParagraphStyle *paragraphStyle = NSMutableParagraphStyle.new; paragraphStyle.alignment = NSTextAlignmentCenter; NSDictionary *attributes = @{NSForegroundColorAttributeName:RGB(0x999999), NSFontAttributeName:[UIFont fontWithName:@"HelveticaNeue-Light" size:10], NSParagraphStyleAttributeName:paragraphStyle}; [str addAttributes:attributes range:NSMakeRange(0, [str length])]; [str addAttribute: NSLinkAttributeName value:urlString range:range]; return str; } 

Deberías llamarlo así:

 NSString *fullString = @"A man who bought the Google.com domain name for $12 and owned it for about a minute has been rewarded by Google for uncovering the flaw."; NSString *linkString = @"Google.com"; NSString *urlString = @"http://www.google.com"; _youTextView.attributedText = [self linkedStringFromFullString:fullString withLinkString:linkString andUrlString:urlString]; 

Simplemente encuentre una solución sin código para UITextView: enter image description here

Habilitar Detección-> ¡Se detectarán las opciones de enlaces, la URL y también el correo electrónico!

Actualizar:

Hubo 2 partes clave en mi pregunta:

  1. Cómo hacer un enlace donde el texto que se muestra para el enlace en el que se puede hacer clic es diferente al enlace real que se invoca:
  2. Cómo configurar los enlaces sin tener que usar código personalizado para establecer los atributos en el texto.

Resulta que iOS 7 agregó la capacidad de cargar texto atribuido desde NSData .

UITextView una subclase personalizada de UITextView que aprovecha el atributo @IBInspectable y le permite cargar contenidos desde un archivo RTF directamente en IB. Simplemente escriba el nombre del archivo en IB y la clase personalizada hará el rest.

Aquí están los detalles:

En iOS 7, NSAttributedString ganó el método initWithData:options:documentAttributes:error: Ese método le permite cargar un NSAttributedString desde un objeto NSData. Primero puede cargar un archivo RTF en NSData, luego use initWithData:options:documentAttributes:error: para cargar ese NSData en su vista de texto. (Tenga en cuenta que también hay un método initWithFileURL:options:documentAttributes:error: que cargará una cadena atribuida directamente desde un archivo, pero ese método quedó obsoleto en iOS 9. Es más seguro utilizar el método initWithData:options:documentAttributes:error: , que no fue desaprobado.

Quería un método que me permitiera instalar enlaces clicables en mis vistas de texto sin tener que crear ningún código específico para los enlaces que estaba usando.

La solución que se me ocurrió fue crear una subclase personalizada de UITextView. RTF_UITextView y le doy una propiedad RTF_Filename llamada RTF_Filename . Agregar el atributo @IBInspectable a una propiedad hace que Interface Builder @IBInspectable esa propiedad en “Attributes Inspector”. Luego puede establecer ese valor desde IB sin código personalizado.

También agregué un atributo @IBDesignable a mi clase personalizada. El atributo @IBDesignable le dice a Xcode que debe instalar una copia en ejecución de su clase de vista personalizada en el constructor de Interfaces para que pueda verlo en la pantalla gráfica de su jerarquía de vistas. () Desafortunadamente, para esta clase, la propiedad @IBDesignable parece ser escamosa. Funcionó cuando lo agregué por primera vez, pero luego eliminé el contenido de texto sin formato de mi vista de texto y los enlaces clicables en mi opinión se fueron y no pude recuperarlos).

El código para mi RTF_UITextView es muy simple. Además de agregar el atributo @IBDesignable y una propiedad RTF_Filename con el atributo @IBInspectable , agregué un método didSet() a la propiedad RTF_Filename . El método didSet() se llama cuando cambia el valor de la propiedad RTF_Filename . El código para el método didSet() es bastante simple:

 @IBDesignable class RTF_UITextView: UITextView { @IBInspectable var RTF_Filename: String? { didSet(newValue) { //If the RTF_Filename is nil or the empty string, don't do anything if ((RTF_Filename ?? "").isEmpty) { return } //Use optional binding to try to get an URL to the //specified filename in the app bundle. If that succeeds, try to load //NSData from the file. if let fileURL = NSBundle.mainBundle().URLForResource(RTF_Filename, withExtension: "rtf"), //If the fileURL loads, also try to load NSData from the URL. let theData = NSData(contentsOfURL: fileURL) { var aString:NSAttributedString do { //Try to load an NSAttributedString from the data try aString = NSAttributedString(data: theData, options: [:], documentAttributes: nil ) //If it succeeds, install the attributed string into the field. self.attributedText = aString; } catch { print("Nerp."); } } } } } 

Tenga en cuenta que si la propiedad @IBDesignable no le permitirá obtener una vista previa de su texto con estilo en el constructor Interfaz, entonces sería mejor establecer el código anterior como una extensión de UITextView en lugar de una subclase personalizada. De esta forma, podría usarlo en cualquier vista de texto sin tener que cambiar la vista de texto a la clase personalizada.

Vea mi otra respuesta si necesita soportar versiones de iOS anteriores a iOS 7.

Puede descargar un proyecto de ejemplo que incluye esta nueva clase de gitHub:

Proyecto de demostración DatesInSwift en Github

Versión Swift:

  // Attributed String for Label let plainText = "Apkia" let styledText = NSMutableAttributedString(string: plainText) // Set Attribuets for Color, HyperLink and Font Size let attributes = [NSFontAttributeName: UIFont.systemFontOfSize(14.0), NSLinkAttributeName:NSURL(string: "http://apkia.com/")!, NSForegroundColorAttributeName: UIColor.blueColor()] styledText.setAttributes(attributes, range: NSMakeRange(0, plainText.characters.count)) registerLabel.attributedText = styledText 

Necesitaba seguir usando un UILabel puro, así llamado esto desde mi reconocedor de tomas (esto se basa en la respuesta de malex aquí: Índice de caracteres en el punto de contacto para UILabel )

 UILabel* label = (UILabel*)gesture.view; CGPoint tapLocation = [gesture locationInView:label]; // create attributed string with paragraph style from label NSMutableAttributedString* attr = [label.attributedText mutableCopy]; NSMutableParagraphStyle* paragraphStyle = [NSMutableParagraphStyle new]; paragraphStyle.alignment = label.textAlignment; [attr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, label.attributedText.length)]; // init text storage NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attr]; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; [textStorage addLayoutManager:layoutManager]; // init text container NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(label.frame.size.width, label.frame.size.height+100) ]; textContainer.lineFragmentPadding = 0; textContainer.maximumNumberOfLines = label.numberOfLines; textContainer.lineBreakMode = label.lineBreakMode; [layoutManager addTextContainer:textContainer]; // find tapped character NSUInteger characterIndex = [layoutManager characterIndexForPoint:tapLocation inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:NULL]; // process link at tapped character [attr enumerateAttributesInRange:NSMakeRange(characterIndex, 1) options:0 usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { if (attrs[NSLinkAttributeName]) { NSString* urlString = attrs[NSLinkAttributeName]; NSURL* url = [NSURL URLWithString:urlString]; [[UIApplication sharedApplication] openURL:url]; } }]; 

Una rápida adición a la descripción original de Duncan C con respecto al comportamiento IB. Él escribe: “Es trivial hacer que los hipervínculos se puedan hacer clic en una UITextView. Simplemente establece la checkbox” detectar enlaces “en la vista en IB, y detecta los enlaces http y los convierte en hipervínculos”.

Mi experiencia (al menos en xcode 7) es que también debes quitar el clic en el comportamiento “Editable” para que las URL sean detectadas y cliqueables.

Si desea utilizar NSLinkAttributeName en una UITextView, puede considerar el uso de la biblioteca AttributedTextView. Es una subclase UITextView que hace que sea muy fácil de manejar. Para obtener más información, consulte: https://github.com/evermeer/AttributedTextView

Puede hacer que cualquier parte del texto interactúe así (donde textView1 es un IBoutlet UITextView):

 textView1.attributer = "1. ".red .append("This is the first test. ").green .append("Click on ").black .append("evict.nl").makeInteract { _ in UIApplication.shared.open(URL(string: "http://evict.nl")!, options: [:], completionHandler: { completed in }) }.underline .append(" for testing links. ").black .append("Next test").underline.makeInteract { _ in print("NEXT") } .all.font(UIFont(name: "SourceSansPro-Regular", size: 16)) .setLinkColor(UIColor.purple) 

Y para manejar hashtags y menciones puedes usar código como este:

 textView1.attributer = "@test: What #hashtags do we have in @evermeer #AtributedTextView library" .matchHashtags.underline .matchMentions .makeInteract { link in UIApplication.shared.open(URL(string: "https://twitter.com\(link.replacingOccurrences(of: "@", with: ""))")!, options: [:], completionHandler: { completed in }) } 

La excelente biblioteca de @AliSoftware OHAttributedStringAdditions hace que sea más fácil agregar enlaces en UILabel aquí está la documentación: https://github.com/AliSoftware/OHAttributedStringAdditions/wiki/link-in-UILabel

si quieres una subcadena activa en tu UITextView, puedes usar mi TextView extendido … es corto y simple. Puede editarlo como lo desee.

resultado: enter image description here

código: https://github.com/marekmand/ActiveSubstringTextView

 NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:strSomeTextWithLinks]; NSDictionary *linkAttributes = @{NSForegroundColorAttributeName: [UIColor redColor], NSUnderlineColorAttributeName: [UIColor blueColor], NSUnderlineStyleAttributeName: @(NSUnderlinePatternSolid)}; customTextView.linkTextAttributes = linkAttributes; // customizes the appearance of links textView.attributedText = attributedString; 

PUNTOS CLAVE:

  • Asegúrese de habilitar el comportamiento “seleccionable” de UITextView en XIB.
  • Asegúrese de desactivar el comportamiento “editable” de UITextView en XIB.

Utilice UITextView y establezca dataDetectorTypes for Link.

Me gusta esto:

 testTextView.editable = false testTextView.dataDetectorTypes = .link 

Si desea detectar el enlace, el número de teléfono, la dirección, etc.

 testTextView.dataDetectorTypes = .all