How can I add different line spacing for different parts of NSAttributedString?

若如初见. 提交于 2019-12-23 23:57:54

问题


I want to have a single NSAttributedString contain multiple messages. If a single message has a long text and it wraps around, I want to have a line spacing of, say, 5. Because I have a single NSAttributedString contain multiple messages, I want to have a bigger line spacing between each message; let's say 20.

What I want

The 'I see' is one message. The 'I'd think it'd be both...' is one message, although it wraps down to two lines and 'Like a one way chat' is one message.

Notice how the line spacing between the 2nd and 3rd is smaller than the 1st and 2nd and between the 3rd and 4th.

What I've tried

I am appending a \n to the end of each message and I've tried using NSParagraphStyle which gives me control of the line spacing, but it seems to be all or nothing:

        // index is the index of the group of messages as I iterate through them
        // contentText is an NSMutableAttributedString
        if index != messages.count - 1 {
            let style = NSMutableParagraphStyle()
            style.lineSpacing = 40.0

            let lineReturn = NSMutableAttributedString(string: "\n")
            contentText.appendAttributedString(lineReturn)

            if index == 0 {
                contentText.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSMakeRange(contentText.length-lineReturn.length, lineReturn.length))
            } else {
                contentText.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSMakeRange(contentText.length-lineReturn.length-1, lineReturn.length+1))
            }
        }

If I add the line spacing to the beginning it will set the line spacing for the entire label.

        if index == 0 {
            let style = NSMutableParagraphStyle()
            style.lineSpacing = 40.0
            contentText.addAttribute(NSParagraphStyleAttributeName, value: style1, range: NSMakeRange(start, 1))
        }

(This is really only my latest try.)

Thanks for any help! :)


回答1:


Details

  • Have very basic custom markup in your English message so you can parse out the different pieces

  • Instruct your translators to leave the markup in and translate the
    rest Have a UIView that can serve as the container of this message

  • Break your English message up in pieces to separate the regular text from the clickable text

  • For each piece create a UILabel on the container UIView

  • For the clickable pieces, set your styling, allow user interaction
    and create your tap gesture recognizer

  • Do some very basic bookkeeping to place the words perfectly across
    the lines

    For Understand.

In the view controller's viewDidLoad I placed this:

[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.")];

I'm calling a method that will build the message. Note the markup I came up with. You can of course invent your own, but key is that I also mark the ends of each clickable region because they span over multiple words.

Here's the method that puts the message together -- see below. First I break up the English message over the # character (or rather @"#" string). That way I get each piece for which I need to create a label separately. I loop over them and look for my basic markup of <ts> and <pp> to detect which pieces are links to what. If the chunk of text I'm working with is a link, then I style a bit and set up a tap gesture recogniser for it. I also strip out the markup characters of course. I think this is a really easy way to do it.

Note some subtleties like how I handle spaces: I simply take the spaces from the (localised) string. If there are no spaces (Chinese, Japanese), then there won't be spaces between the chunks either. If there are spaces, then those automatically space out the chunks as needed (e.g. for English). When I have to place a word at the start of a next line though, then I do need to make sure that I strip of any white space prefix from that text, because otherwise it doesn't align properly.

- (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's 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;
  }
}

if you want to use gesture then use this method.

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

Hope this helps. I'm sure there are much smarter and more elegant ways to do this, but this is what I was able to come up with and it works nicely.

this answer display output like following screen shot...but you got idea from this answer.




回答2:


Gotcha!

You need to play with baselineOffset attribute:

let contentText = NSMutableAttributedString(
string: "I see\nI'd think it`d be both a notification and a\nplace to see past announcements\nLike a one way chat.")

contentText.addAttribute(.baselineOffset, value: 10, range: NSRange(location: 0, length: 5))
contentText.addAttribute(.baselineOffset, value: -10, range: NSRange(location: 85, length: 20))


Result:

"I see

I'd think it`d be both a notification and a
place to see past announcements

Like a one way chat."



来源:https://stackoverflow.com/questions/34215642/how-can-i-add-different-line-spacing-for-different-parts-of-nsattributedstring

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!