14

In my iOS app I'm using NSAttributedString to generate a list of bullets. Unfortunately I'm struggling with making the bullets look presentable. My first attempt was to use regular text and a unicode character for the bullets, basically using a string like this:

var attributedString = NSMutableAttributedString(
    string: "Here is a list of bullets and a paragraph introducing them, note that this paragraph spans multiple lines\n" +
    "• This is the first bullet\n" +
    "• Here is a second bullet\n" +
    "• And here is a third bullet with a lot of text such that it overflows to the next line"
)

The result was this:

enter image description here

I like how the bullets look, but the overflowing text in the last bullet should be aligned with the line before and I couldn't figure out how to achieve that with plain text (without applying the same alignment to the paragraph above).

My 2nd attempt was to use html in NSAttributedString via NSHTMLTextDocumentType, and use <ul> and <li> elements to generate the bullets.

let content = "Here is a list of bullets and a paragraph introducing them, note that this paragraph spans multiple lines" +
    "<ul>" +
         "<li>This is the first bullet</li>" +
         "<li>Here is a second bullet</li>" +
         "<li>And here is a third bullet with a lot of text such that it overflows to the next line</li>" +
    "</ul>"
var attributedString = try! NSMutableAttributedString(
    data: content,
    options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
    documentAttributes: nil
)

That fixed the first issue but introduced a new one:

enter image description here

The bullets are now spaced too far (both from the left edge and the text on the right). I tried to use the typical html/css tricks to fix the alignment (<li style="text-indent: -10px;">) but those styles seem to be ignored by NSAttributedString.

I tried to remedy this with an additional NSMutableParagraphStyle but it seems to do more harm than good. Here is what I tried:

var paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.firstLineHeadIndent = 0
paragraphStyle.headIndent = 20
attributedString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: attributedStringRange)

And here is what I got:

enter image description here

As you can see, it only made things worse, here are my issues with it:

  • It does offset the 2nd line, but I only want that effect for the bullets, not the paragraph before (I guess I can remedy that by decreasing the range over which the effect gets applied)
  • I have to guess/hardcode the offset, in my example I picked 20 and that wasn't enough for the bullets given the current font settings, which could change
  • For some reason the bullet spacing is even more stretched out now for no reason at all, it seems like just applying a vanilla NSParagraphStyle without doing anything does this and I see no option to fix this.

All I really want is for my bullet spacing to look similar to the first screenshot while the overflow indent for second line to look like the second without having to hardcode exact pixel positions. Could you guys help me out?

Thanks

Larme
  • 24,190
  • 6
  • 51
  • 81
Alexander Tsepkov
  • 3,946
  • 3
  • 35
  • 59
  • Were you able to find a nice solution for this? – flohei Sep 26 '16 at 13:09
  • Unfortunately I no longer remember what the solution was, I think it was some variation of the posted answer. I think I ended up using separate elements for the top paragraph and the bullet list, which felt like a hack but wasn't worth spending more time on. – Alexander Tsepkov Sep 26 '16 at 17:09
  • I see. That's what I'm trying to avoid as I get the entire text from our API and I don't really want to mess with the results as they might vary dramatically. So I'd probably end up with exceptions over exceptions. Anyway, thanks for answering! – flohei Sep 27 '16 at 05:29
  • So what did you do? I have the exact problems – Sonic Master Feb 19 '18 at 13:41

3 Answers3

8

I had to add custom styles to a list and this is the paragraph style I ended up using for each bullet in an NSAttributedString

The headIndent and firstLineHeadIndent can be changed but the NSTextTab location should be the same as headIndent

NSMutableParagraphStyle *const bulletParagraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
bulletParagraphStyle.headIndent = 60;
bulletParagraphStyle.firstLineHeadIndent = 30;
NSTextTab *listTab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentNatural location:60 options:@{}];
bulletParagraphStyle.tabStops = @[listTab];

This works assuming your bullet point has a \t after it (and then then text)

lostInTransit
  • 70,519
  • 61
  • 198
  • 274
  • 1
    Thanks, this almost works, now I'm just trying to separate the indent of the top paragraph from the bullet (trying with NSRange now and it's not being cooperative, so I'll come back to this once I figure it out). – Alexander Tsepkov May 25 '16 at 15:11
  • 1
    have same issue, easilly fixed it using html layout but not really like using tables
    – Injectios Sep 20 '16 at 12:52
5

Use <table> instead of <ul><li> to handle the bullet alignment. Styles like ul{margin:0;padding:0} or li{margin-left: 0;} will be ignored.

Example:

let html = "<table>" +
            "<tr><td valign=\"top\" style=\"padding-right:16px\">•</td><td valign=\"top\">text 1</td></tr>" +
            "<tr><td valign=\"top\" style=\"padding-right:16px\">•</td><td valign=\"top\">text 2</td></tr>" +
            "</table>"

let attributedString = try! NSAttributedString(data: html.data(using: String.Encoding.utf8)!,
                               options: [.documentType: NSAttributedString.DocumentType.html,
                                         .characterEncoding: String.Encoding.utf8.rawValue,],
                               documentAttributes: nil)
Peter Kreinz
  • 7,979
  • 1
  • 64
  • 49
0

And here is alternate way I did this:

    attributedString.enumerateAttribute(.paragraphStyle,
                                        in: NSRange(location: 0, length: attributedString.length),
                                        options: [])
    { (paragraphStyle, range, _) in
        guard let paragraphStyle = paragraphStyle as? NSParagraphStyle else { return }
        let updatedStyle = NSMutableParagraphStyle()
        updatedStyle.setParagraphStyle(paragraphStyle)
        
        // Here we can modify paragraphs. Also could add condition to update only paragraphs with lists.
        updatedStyle.firstLineHeadIndent = 0
        updatedStyle.headIndent = 20
        
        attributedString.addAttribute(.paragraphStyle, value: updatedStyle, range: range)
    }

Main idea was instead of making one paragraph (which as I understand brake things) to alter existing paragraphs. To achieve I use enumerateAttribute(...) which give me all existing paragraphs and their ranges. This question point me to this idea: Get the range of a paragraph NSAttributedString

Skie
  • 1,942
  • 16
  • 27