IOS UITextView или UILabel с интерактивными ссылками на действия

Я хочу сделать UILabel или UITextView с текстом с двумя доступными для него ссылками. Не ссылки на веб-страницы, но я хочу связать эти 2 ссылки с такими действиями, как я бы сделал с UIButton. Все примеры, которые я видел, - это ссылки на веб-просмотры, но я этого не хочу. Кроме того, текст будет переведен на другие языки, поэтому позиции должны быть динамическими.

Хотите сделать это:

enter image description here

Ответ 1

Мне нужно было решить эту ту же проблему: очень похожий текст с этими двумя ссылками в нем, по нескольким строкам, и нужно, чтобы он мог быть переведен на любой язык (включая разные порядки слов и т.д.). Я просто решил это, поэтому позвольте мне рассказать, как я это сделал.

Сначала я думал, что должен создать атрибутный текст, а затем сопоставить местоположение касания касания к регионам внутри этого текста. Хотя я думаю, что это выполнимо, я также считаю это слишком сложным подходом.

Вот что я сделал вместо этого:

РЕЗЮМЕ:

  • У вас есть очень простая пользовательская разметка в своем английском сообщении, чтобы вы могли разобрать разные фрагменты.
  • Попросите своих переводчиков оставить разметку и перевести остальные.
  • У UIView, который может служить контейнером этого сообщения
  • Разбейте свое английское сообщение на части, чтобы отделить обычный текст от интерактивного текста
  • Для каждой части создайте UILabel на контейнере UIView
  • Для интерактивных элементов установите стиль, разрешите взаимодействие с пользователем и создайте распознаватель жестов.
  • Сделайте очень простой бухгалтерский учет, чтобы поместить слова в соответствие друг с другом.

ДЕТАЛЬ:

В контроллере представления viewDidLoad я разместил это:

[self buildAgreeTextViewFromString:NSLocalizedString(@"I agree to the #<ts>terms of service# and #<pp>privacy policy#", 
                                                     @"PLEASE NOTE: please translate \"terms of service\" and \"privacy policy\" as well, and leave the #<ts># and #<pp># around your translations just as in the English version of this message.")];

Я вызываю метод, который будет создавать сообщение. Обратите внимание на разметку, с которой я столкнулся. Вы можете, конечно, придумать свое, но ключ в том, что я также отмечаю концы каждого кликабельного региона, потому что они охватывают несколько слов.

Здесь метод, который помещает сообщение вместе - см. ниже. Сначала я разбиваю английское сообщение на символ # (или, скорее, строку @"#"). Таким образом, я получаю каждую часть, для которой мне нужно создать ярлык отдельно. Я петлю над ними и ищу свою основную разметку <ts> и <pp>, чтобы определить, какие части являются ссылками на что. Если кусок текста, с которым я работаю, является ссылкой, то я немного стилю и создаю для него распознаватель жестов. Разумеется, я также выделяю символы разметки. Я думаю, что это действительно простой способ сделать это.

Обратите внимание на некоторые тонкости, например, как обрабатывать пробелы: я просто беру пробелы из (локализованной) строки. Если нет пробелов (китайский, японский), то между кусками также не должно быть пробелов. Если есть пробелы, то они автоматически отсеивают куски по мере необходимости (например, для английского). Когда мне нужно поставить слово в начале следующей строки, тогда мне нужно убедиться, что я убираю любой префикс пробела из этого текста, потому что в противном случае он не будет правильно выровняться.

- (void)buildAgreeTextViewFromString:(NSString *)localizedString
{
  // 1. Split the localized string on the # sign:
  NSArray *localizedStringPieces = [localizedString componentsSeparatedByString:@"#"];

  // 2. Loop through all the pieces:
  NSUInteger msgChunkCount = localizedStringPieces ? localizedStringPieces.count : 0;
  CGPoint wordLocation = CGPointMake(0.0, 0.0);
  for (NSUInteger i = 0; i < msgChunkCount; i++)
  {
    NSString *chunk = [localizedStringPieces objectAtIndex:i];
    if ([chunk isEqualToString:@""])
    {
      continue;     // skip this loop if the chunk is empty
    }

    // 3. Determine what type of word this is:
    BOOL isTermsOfServiceLink = [chunk hasPrefix:@"<ts>"];
    BOOL isPrivacyPolicyLink  = [chunk hasPrefix:@"<pp>"];
    BOOL isLink = (BOOL)(isTermsOfServiceLink || isPrivacyPolicyLink);

    // 4. Create label, styling dependent on whether it a link:
    UILabel *label = [[UILabel alloc] init];
    label.font = [UIFont systemFontOfSize:15.0f];
    label.text = chunk;
    label.userInteractionEnabled = isLink;

    if (isLink)
    {
      label.textColor = [UIColor colorWithRed:110/255.0f green:181/255.0f blue:229/255.0f alpha:1.0];
      label.highlightedTextColor = [UIColor yellowColor];

      // 5. Set tap gesture for this clickable text:
      SEL selectorAction = isTermsOfServiceLink ? @selector(tapOnTermsOfServiceLink:) : @selector(tapOnPrivacyPolicyLink:);
      UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self
                                                                                   action:selectorAction];
      [label addGestureRecognizer:tapGesture];

      // Trim the markup characters from the label:
      if (isTermsOfServiceLink) 
        label.text = [label.text stringByReplacingOccurrencesOfString:@"<ts>" withString:@""];
      if (isPrivacyPolicyLink)  
        label.text = [label.text stringByReplacingOccurrencesOfString:@"<pp>" withString:@""];
    }
    else
    {
      label.textColor = [UIColor whiteColor];
    }

    // 6. Lay out the labels so it forms a complete sentence again:

    // If this word doesn't fit at end of this line, then move it to the next
    // line and make sure any leading spaces are stripped off so it aligns nicely:

    [label sizeToFit];

    if (self.agreeTextContainerView.frame.size.width < wordLocation.x + label.bounds.size.width)
    {
      wordLocation.x = 0.0;                       // move this word all the way to the left...
      wordLocation.y += label.frame.size.height;  // ...on the next line

      // And trim of any leading white space:
      NSRange startingWhiteSpaceRange = [label.text rangeOfString:@"^\\s*"
                                                          options:NSRegularExpressionSearch];
      if (startingWhiteSpaceRange.location == 0)
      {
        label.text = [label.text stringByReplacingCharactersInRange:startingWhiteSpaceRange
                                                         withString:@""];
        [label sizeToFit];
      }
    }

    // Set the location for this label:
    label.frame = CGRectMake(wordLocation.x,
                             wordLocation.y,
                             label.frame.size.width,
                             label.frame.size.height);
    // Show this label:
    [self.agreeTextContainerView addSubview:label];

    // Update the horizontal position for the next word:
    wordLocation.x += label.frame.size.width;
  }
}

И вот мои методы, которые обрабатывают обнаруженные ответвления на этих ссылках.

- (void)tapOnTermsOfServiceLink:(UITapGestureRecognizer *)tapGesture
{
  if (tapGesture.state == UIGestureRecognizerStateEnded)
  {
    NSLog(@"User tapped on the Terms of Service link");
  }
}


- (void)tapOnPrivacyPolicyLink:(UITapGestureRecognizer *)tapGesture
{
  if (tapGesture.state == UIGestureRecognizerStateEnded)
  {
    NSLog(@"User tapped on the Privacy Policy link");
  }
}

Надеюсь, это поможет. Я уверен, что есть намного более умные и более элегантные способы сделать это, но это то, что я смог придумать, и он работает хорошо.

Вот как это выглядит в приложении:

Simulator screenshot of the end result

Удачи!: -)

Эрик

Ответ 2

Как я реализую пользовательские текстовые действия (например, кнопку) для UITextView:

Основные принципы:

  • Используйте NSAttributedString как способ определения ссылки для доступа.
  • Используйте UITextViewDelegate, чтобы поймать нажатие ссылки.

Определить строку URL:

private let kURLString = "myapp://fakeActionUrl"

Добавить ссылку на вашу атрибутную строку:

let range = NSMakeRange(0, actionString.characters.count)
mutableAttributedString.addAttribute(NSLinkAttributeName, value: kURLString, range: range)

Присвоить атрибутированную строку текстовому виду:

textView.attributedText = attributedString

Внедрить UITextViewDelegate (это действительно ключевая часть, которая запрещает URL-адресу открывать какой-либо веб-сайт и где вы можете определить свое настраиваемое действие):

func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
    if (URL.absoluteString == kURLString) {
        // Do whatever you want here as the action to the user pressing your 'actionString'
    }
    return false
}

Вы также можете настроить свои атрибуты ссылок:

textView.linkTextAttributes = [NSForegroundColorAttributeName : UIColor.red, NSUnderlineStyleAttributeName : NSUnderlineStyle.styleSingle]

Как я реализую пользовательские действия для UILabel:

Обычно я использую TTTAttributedLabel.

Ответ 3

Вот полный пример, сделанный в Swift 2 без стручков.

import UIKit

class SomeViewController: UIViewController, UITextViewDelegate {
  @IBOutlet weak var terms: UITextView!

  let termsAndConditionsURL = "http://www.example.com/terms";
  let privacyURL = "http://www.example.com/privacy";

  override func viewDidLoad() {
    super.viewDidLoad()

    self.terms.delegate = self
    let str = "By using this app you agree to our Terms and Conditions and Privacy Policy"
    let attributedString = NSMutableAttributedString(string: str)
    var foundRange = attributedString.mutableString.rangeOfString("Terms and Conditions")
    attributedString.addAttribute(NSLinkAttributeName, value: termsAndConditionsURL, range: foundRange)
    foundRange = attributedString.mutableString.rangeOfString("Privacy Policy")
    attributedString.addAttribute(NSLinkAttributeName, value: privacyURL, range: foundRange)
    terms.attributedText = attributedString
  }

  func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
    if (URL.absoluteString == termsAndConditionsURL) {
      let myAlert = UIAlertController(title: "Terms", message: nil, preferredStyle: UIAlertControllerStyle.Alert)
      myAlert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
      self.presentViewController(myAlert, animated: true, completion: nil)
    } else if (URL.absoluteString == privacyURL) {
      let myAlert = UIAlertController(title: "Conditions", message: nil, preferredStyle: UIAlertControllerStyle.Alert)
      myAlert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
      self.presentViewController(myAlert, animated: true, completion: nil)
    }
    return false
  }
}

Ответ 4

Проверьте этот класс UILabel, это, безусловно, поможет вам. Я сделал то же самое, используя это.

TTTAttributedLabel

Ответ 5

Вот переведенная версия принятого ответа на С# для Xamarin для тех, кто сочтет это полезным:

        var str = "Or, #<li>log in# to see your orders."; 
        var strParts = str.Split('#');
        var ptWordLocation = new PointF (0, 0);

        if (strParts.Length > 1) {
            //Loop the parts of the string
            foreach (var s in strParts) {
                //Check for empty string
                if (!String.IsNullOrEmpty (s)) {
                    var lbl = new UILabel ();
                    lbl.Font = lbl.Font.WithSize (15);
                    lbl.TextColor = cpAppConstants.TextColorMessage;
                    lbl.UserInteractionEnabled = s.Contains ("<li>");
                    lbl.Text = s.Replace ("<li>", "");

                    if (s.Contains ("<li>")) {
                        lbl.TextColor = UIColor.FromRGB (200, 95, 40);

                        //Set tap gesture for this clickable text:
                        var gesture = new UITapGestureRecognizer ();
                        gesture.AddTarget(() => buildLoginLabel_onTap(gesture));
                        lbl.AddGestureRecognizer (gesture);
                    }

                    lbl.SizeToFit ();

                    //Lay out the labels so it forms a complete sentence again
                    if (vw.Frame.Width < ptWordLocation.X + lbl.Bounds.Size.Width) {
                        ptWordLocation.X = 0f;
                        ptWordLocation.Y += lbl.Frame.Size.Height;
                        lbl.Text.Trim ();
                    }

                    lbl.Frame = new RectangleF (ptWordLocation.X, ptWordLocation.Y, lbl.Frame.Size.Width, lbl.Frame.Size.Height);
                    vw.AddSubview (lbl);

                    //Update the horizontal width
                    ptWordLocation.X += lbl.Frame.Size.Width;
                }
            }
        }

Ответ 6

Нажмите здесь, чтобы узнать, как установить Listener для textView

и Add

     UITapGestureRecognizer *listener = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapAction:)];

Напишите действие, которое вы хотите сделать в

 - (void)tapAction:(UITapGestureRecognizer *)sender
{
}

Добавить слушателя в представление

      [self.view addGestureRecognizer:listener];

Ответ 7

Я использовал решение Erik, но ему нужно было сделать это с помощью Swift. После преобразования я обнаружил небольшую проблему, когда, если у вас есть много текста (более одной строки), прежде чем вы попали в ссылку, тогда он не был обернут правильно, поэтому я добавил функцию, соответствующую тексту.

func setText(newText:String){

    // 1. Split the localized string on the # sign:
    let localizedStringPieces:NSArray = newText.componentsSeparatedByString("#")

    // 2. Loop through all the pieces:
    var msgChunkCount:Int = localizedStringPieces.count

    var wordLocation:CGPoint = CGPointMake(0.0, 0.0)

    for (var i:Int = 0; i < msgChunkCount; i++){

        let chunk:String = localizedStringPieces[i] as! String

        if chunk == ""{
            continue;     // skip this loop if the chunk is empty
        }

        // 3. Determine what type of word this is:
        let isTermsOfServiceLink:Bool = chunk.hasPrefix("<ts>")
        let isPrivacyPolicyLink:Bool  = chunk.hasPrefix("<pp>")
        let isLink:Bool = (Bool)(isTermsOfServiceLink || isPrivacyPolicyLink)


        var remainingText:String = chunk

        while count(remainingText)>0{

            // 4. Create label, styling dependent on whether it a link:
            let label:UILabel = UILabel()
            label.font = UIFont.systemFontOfSize(methodFontSize)
            label.text = remainingText
            label.userInteractionEnabled = isLink

            if (isLink){
                label.textColor = UIColor(red: 110/255, green: 181/255, blue: 229/255, alpha: 1.0)
                label.highlightedTextColor = UIColor.yellowColor()

                // 5. Set tap gesture for this clickable text:
                var selectorAction:Selector =  isTermsOfServiceLink ? "tapOnTermsOfServiceLink" : "tapOnPrivacyPolicyLink"

                let tapGesture:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: selectorAction)

                label.addGestureRecognizer(tapGesture)

                // Trim the markup characters from the label:
                if (isTermsOfServiceLink){
                    label.text = label.text?.stringByReplacingOccurrencesOfString("<ts>", withString: "", options: nil, range: nil)
                }
                if (isPrivacyPolicyLink){
                    label.text = label.text?.stringByReplacingOccurrencesOfString("<pp>", withString: "", options: nil, range: nil)
                }
            }else{
                label.textColor = UIColor.whiteColor()
            }

            // If this chunk of text doesn't fit at end of this line, then move it to the next
            // line and make sure any leading spaces are stripped off so it aligns nicely:

            label.sizeToFit()

            let labelHeight = label.frame.size.height

            var leftOverText:String = fitLabelToWidth(label, width: self.textContainer.frame.size.width - wordLocation.x)

            // if we can't fit anything onto this line then drop down
            if label.text == "" {
                //drop to a new line
                wordLocation.x = 0.0                       // move this word all the way to the left...

                wordLocation.y += labelHeight;  // ...on the next line.  (Have to use a constant here because if label has no text it also has no height)

                // refit the text
                label.text = remainingText
                leftOverText = fitLabelToWidth(label, width: self.textContainer.frame.size.width - wordLocation.x)

                //NB WE ARE ASSUMING HERE THAT AFTER DROPPING DOWN AT LEAST SOME OF THIS TEXT WILL FIT
                // IF THIS ISN'T THE CASE THEN THE LINE WOULD ALWAYS BE TOO BIG AND WE WOULD NEVER BE ABLE TO FIT IT ON ANYWAY!
            }

            // Set the location for this label:
            label.frame = CGRectMake(wordLocation.x, wordLocation.y, label.frame.size.width, label.frame.size.height)

            // Show this label:
            self.textContainer.addSubview(label)

            // Update the horizontal position for the next word:
            wordLocation.x += label.frame.size.width;

            // update our remaining text and get ready to go again
            remainingText = leftOverText
        }

    }

}

// fit the text label (formatted externally) to the desired with, chopping off text to make it so
// return the remaining text that didn't make the cut as a string
func fitLabelToWidth(label:UILabel, width:CGFloat)->String{
    let startingText:String = label.text!
    println("Trying to fit ::\(startingText)::")


    // if the string is null then we are done
    if startingText == ""{
        return ""
    }

    // if this fits already then we are done
    label.sizeToFit()
    if label.frame.size.width <= width{
        return ""
    }

    // so now we have to loop round trying to get this to fit
    var cutRange:Range<String.Index> = Range<String.Index>(start: startingText.startIndex, end: startingText.startIndex)
    var searchRange:Range<String.Index>

    var startSearchIndex:String.Index = startingText.startIndex
    var lastSearchIndex:String.Index = startSearchIndex

    var testText:String = ""
    var lastText:String = ""
    label.text = testText
    label.sizeToFit()

    while label.frame.size.width <= width{

        // store off the last used text as this might be as far as we go
        lastText = testText
        lastSearchIndex = startSearchIndex

        // set up the search range so we look for spaces missing out any previous ones
        searchRange = Range<String.Index>(start: startSearchIndex, end: startingText.endIndex)

        // cut out a range with the next occurrence of spaces
        cutRange = startingText.rangeOfString(" ", options: NSStringCompareOptions.CaseInsensitiveSearch, range: searchRange, locale: nil)!

        // get some text from the start of the string to our cut point (start)
        testText = startingText.substringToIndex(cutRange.startIndex)

        // move the search start to the point after the end of the spaces we just found
        startSearchIndex = cutRange.endIndex

        // try this in our label to see if it sizes ok
        label.text = testText
        label.sizeToFit()


    }

    // we leave the while when the string gets too big
    label.text = lastText
    label.sizeToFit()

    return startingText.substringFromIndex(lastSearchIndex)

}

Ответ 8

Вы можете использовать нижеприведенный код, чтобы добавить жест нажатия на UILable: -

Шаг 1:

Delegate "UIGestureRecognizerDelegate" to your viewcontroller.h 

for example: 
  @interface User_mail_List : UIViewController<UIGestureRecognizerDelegate>

Шаг 2:

//create you UILable
UILabel *title_lbl= [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 30)];
[title_lbl setText:@"u&me"];
[title_lbl setUserInteractionEnabled:YES];
[yourView addSubview:title_lbl];

Шаг 3:

UITapGestureRecognizer *tap= [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(Prof_lbl_Pressed:)];//your action selector
[tap setNumberOfTapsRequired:1];
title_lbl.userInteractionEnabled= YES;
[title_lbl addGestureRecognizer:tap];

Шаг 4:

-(void)Prof_lbl_Pressed:(id)sender{
   //write your code action
}

спасибо,

Ответ 9

Мое решение для интерактивных ссылок на действие - это

myLabel.automaticLinkDetectionEnabled = YES;
myLabel.urlLinkTapHandler = ^(KILabel *myLabel, NSString *string, NSRange range) {
            [self attemptOpenURL:[NSURL URLWithString:string]];
            NSLog(@"URL tapped %@", string);
        };

Проверьте этот класс UILabel, это поможет вам.

https://github.com/Krelborn/KILabel

Ответ 10

Вы можете использовать несколько перекрывающихся UILabel с userInteractionEnabled = YES на нем и добавить UITapGestureRecognizer на эту метку с разными жирными шрифтами.

Здесь является одним из таких примеров.

Что-то как это также можно попробовать.

Если вы хотите работать с этим решением, вы можете попробовать "Fancy-Label" . Найдите в этой ссылке для текста "Heres my implementation" и нажмите на нее. Вы будете готовы использовать продукт. Не забудьте нажать кнопку "Переключить" в приложении, которое вы используете, используя приведенный выше пример.

Я надеюсь, что вам это поможет.