Como posso criar um link clicável em um NSAttributedString?

É trivial fazer hiperlinks clicáveis ​​em um UITextView . Você acabou de definir a checkbox de seleção “detectar links” na exibição no IB, e detecta links HTTP e os transforma em hiperlinks.

No entanto, isso ainda significa que o que o usuário vê é o link “bruto”. Arquivos RTF e HTML permitem que você configure uma string legível pelo usuário com um link “por trás”.

É fácil instalar o texto atribuído em uma exibição de texto (ou em um UILabel ou UITextField ). No entanto, quando esse texto atribuído inclui um link, ele não é clicável.

Existe uma maneira de tornar o texto legível pelo usuário clicável em um UITextView , UILabel ou UITextField ?

A marcação é diferente em SO, mas aqui está a ideia geral. O que eu quero é um texto como este:

Esta metamorfose foi gerada com o Face Dancer , Clique para ver na loja de aplicativos.

A única coisa que posso conseguir é isto:

Essa metamorfose foi gerada com o Face Dancer. Clique em http://example.com/facedancer para visualizar na loja de aplicativos.

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 :

Isso não é diretamente sobre a questão, mas apenas para esclarecer, UITextField e UILabel não suporta URLs de abertura. Se você quiser usar o UILabel com links, você pode verificar TTTAttributedLabel .

Além disso, você deve definir o valor de dataDetectorTypes do seu UITextView para UIDataDetectorTypeLink ou UIDataDetectorTypeAll para abrir URLs quando clicado. Ou você pode usar o método delegado conforme sugerido nos comentários.

Eu achei isso muito útil, mas eu precisei fazer isso em alguns lugares, então envolvi minha abordagem em uma extensão simples para 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 } } 

Exemplo de uso:

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

Objetivo-C

Acabei de encontrar um requisito para fazer o mesmo em um projeto Objective-C puro, então aqui está a categoria 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 

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

Pequena melhoria na solução da ujell: Se você usar a NSURL em vez de uma NSString, poderá usar qualquer URL (por exemplo, urls 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; 

Diverta-se!

Acabei de criar uma subclass da UILabel para abordar especificamente esses casos de uso. Você pode adicionar vários links facilmente e definir diferentes manipuladores para eles. Ele também suporta o destaque do link pressionado quando você toca para baixo para feedback de toque. Por favor, consulte https://github.com/null09264/FRHyperLabel .

No seu caso, o código pode gostar disso:

 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 canvas de exemplo (o manipulador está configurado para abrir um alerta em vez de abrir uma url nesse caso)

enfrentado

Eu também tive um requisito semelhante, inicialmente eu usei UILabel e depois percebi que o UITextView é melhor. Eu fiz UITextView se comportar como UILabel desabilitando a interação e rolagem e fez um método de categoria para NSMutableAttributedString para definir o link para o texto mesmo que o que Karl tinha feito (+1 para isso) esta é a minha versão 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]; } } 

você pode usar o delegado abaixo para lidar com a ação

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

Use o UITextView que suporta links clicáveis. Crie uma string atribuída usando o seguinte código

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

Em seguida, defina o texto UITextView da seguinte forma

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

Certifique-se de ativar o comportamento “Selecionável” do UITextView no XIB.

O cerne da minha pergunta era que eu queria ser capaz de criar links clicáveis ​​em visualizações de texto / campos / labels sem ter que escrever código personalizado para manipular o texto e adicionar os links. Eu queria que isso fosse orientado por dados.

Eu finalmente descobri como fazer isso. A questão é que o IB não honra os links incorporados.

Além disso, a versão iOS do NSAttributedString não permite inicializar uma sequência atribuída a partir de um arquivo RTF. A versão OS X do NSAttributedString tem um inicializador que leva um arquivo RTF como input.

NSAttributedString está em conformidade com o protocolo NSCoding, portanto, você pode convertê-lo para / de NSData

Eu criei uma ferramenta de linha de comando do OS X que usa um arquivo RTF como input e gera um arquivo com a extensão .data que contém o NSData do NSCoding. Em seguida, coloco o arquivo .data no meu projeto e adiciono algumas linhas de código que carregam o texto na exibição. O código se parece com isso (este projeto estava no 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 aplicativos que usam muito texto formatado, eu crio uma regra de compilation que informa ao Xcode que todos os arquivos .rtf em uma determinada pasta são de origem e que os arquivos .data são a saída. Depois de fazer isso, simplesmente adiciono os arquivos .rtf ao diretório designado (ou edito os arquivos existentes) e o processo de compilation descobre que eles são novos / atualizados, executa a ferramenta de linha de comando e copia os arquivos no pacote de aplicativos. Isso funciona lindamente.

Eu escrevi uma postagem no blog que leva a um projeto de exemplo (Swift) demonstrando a técnica. Você pode vê-lo aqui:

Criando URLs clicáveis ​​em um UITextField que é aberto no seu aplicativo

Exemplo do Swift 3 para detectar ações em toques de texto atribuídos

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 } 

Da mesma forma, você pode adicionar qualquer ação desejada com o método shouldInteractWith URL UITextFieldDelegate.

Felicidades!!

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 

Eu escrevi um método, que adiciona um link (linkString) a uma string (fullString) com uma certa 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; } 

Você deveria ligar assim:

 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]; 

Basta encontrar uma solução livre de código para o UITextView: insira a descrição da imagem aqui

Ative as opções Detection-> Links, o URL e também o email serão detectados e clicáveis!

Atualizar:

Havia duas partes fundamentais para minha pergunta:

  1. Como criar um link no qual o texto mostrado para o link clicável é diferente do link real invocado:
  2. Como configurar os links sem ter que usar código personalizado para definir os atributos no texto.

Acontece que o iOS 7 adicionou a capacidade de carregar texto atribuído do NSData .

Criei uma subclass personalizada do UITextView que aproveita o atributo @IBInspectable e permite carregar o conteúdo de um arquivo RTF diretamente no IB. Você simplesmente digita o nome do arquivo no IB e a class personalizada faz o resto.

Aqui estão os detalhes:

No iOS 7, o NSAttributedString ganhou o método initWithData:options:documentAttributes:error: Esse método permite carregar um NSAttributedString de um object NSData. Você pode primeiro carregar um arquivo RTF no NSData e, em seguida, usar initWithData:options:documentAttributes:error: para carregar esse NSData em sua textview. (Observe que há também um método initWithFileURL:options:documentAttributes:error: que carregará uma cadeia atribuída diretamente de um arquivo, mas esse método foi preterido no iOS 9. É mais seguro usar o método initWithData:options:documentAttributes:error: , que não foi preterido.

Eu queria um método que me permitisse instalar links clicáveis ​​em minhas visualizações de texto sem precisar criar nenhum código específico para os links que eu estava usando.

A solução que criei foi criar uma subclass personalizada do UITextView que chamo de RTF_UITextView e dar a ela uma propriedade RTF_Filename chamada RTF_Filename . Adicionar o atributo @IBInspectable a uma propriedade faz com que o Interface Builder exponha essa propriedade no “Inspetor de Atributos”. Você pode então definir esse valor de IB sem código personalizado.

Eu também adicionei um atributo @IBDesignable à minha class personalizada. O atributo @IBDesignable informa ao Xcode que ele deve instalar uma cópia em execução de sua class de visualização customizada no construtor Interface para que você possa vê-lo na exibição gráfica de sua hierarquia de visualização. () Infelizmente, para esta class, a propriedade @IBDesignable parece ser esquisita. Ele funcionou quando eu o adicionei pela primeira vez, mas depois apaguei o conteúdo de texto sem formatação da minha textview e os links clicáveis ​​do meu modo de exibição foram embora e eu não consegui recuperá-los.)

O código para o meu RTF_UITextView é muito simples. Além de adicionar o atributo @IBDesignable e uma propriedade RTF_Filename com o atributo @IBInspectable , adicionei um método didSet() à propriedade RTF_Filename . O método didSet() é chamado sempre que o valor da propriedade RTF_Filename alterado. O código para o método didSet() é bastante simples:

 @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."); } } } } } 

Observe que, se a propriedade @IBDesignable não permitir uma visualização confiável do texto com estilo no construtor Interface, talvez seja melhor configurar o código acima como uma extensão do UITextView em vez de uma subclass customizada. Dessa forma, você pode usá-lo em qualquer exibição de texto sem precisar alterar a exibição de texto para a class personalizada.

Veja minha outra resposta se você precisar dar suporte às versões do iOS antes do iOS 7.

Você pode fazer o download de um projeto de amostra que inclui essa nova class do gitHub:

Projeto de demonstração DatesInSwift no Github

Versão rápida:

  // 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 

Eu precisava continuar usando um UILabel puro, assim chamado pelo meu reconhecedor de tap (isso é baseado na resposta do malex aqui: Índice de caracteres no ponto de contato para a 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]; } }]; 

Uma rápida adição à descrição original de Duncan C vis-á-vie comportamento IB. Ele escreve: “É trivial fazer hiperlinks clicáveis ​​em um UITextView. Você acabou de definir a checkbox de seleção” detectar links “na exibição no IB, e detecta links http e os transforma em hiperlinks.”

Minha experiência (pelo menos no xcode 7) é que você também precisa desmarcar o comportamento “Editável” para os URLs serem detectados e clicáveis.

Se você quiser usar o NSLinkAttributeName em um UITextView, considere o uso da biblioteca AttributedTextView. É uma subclass UITextView que facilita o manuseio deles. Para mais informações, consulte: https://github.com/evermeer/AttributedTextView

Você pode fazer qualquer parte do texto interagir assim (onde textView1 é um 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) 

E para lidar com hashtags e menções, você pode 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 }) } 

A excelente biblioteca do @AliSoftware OHAttributedStringAdditions facilita a adição de links no UILabel aqui está a documentação: https://github.com/AliSoftware/OHAttributedStringAdditions/wiki/link-in-UILabel

Se você quiser substring ativo em seu UITextView, então você pode usar meu TextView estendido … é curto e simples. Você pode editá-lo como quiser.

resultado: insira a descrição da imagem aqui

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; 

PONTOS CHAVE:

  • Certifique-se de ativar o comportamento “Selecionável” do UITextView no XIB.
  • Certifique-se de desativar o comportamento “Editável” do UITextView no XIB.

Use o UITextView e defina dataDetectorTypes para Link.

como isso:

 testTextView.editable = false testTextView.dataDetectorTypes = .link 

Se você quiser detectar link, número de telefone, endereço etc.

 testTextView.dataDetectorTypes = .all