'iOS7 TextKit: bullet point alignment

I'm writing an app for iOS 7 only and I'm trying to get decent formatting on bullet points in a non-editable UITextView.

It's easy enough to just insert a bullet point character, but of course the left indentation won't follow. What's the easiest way on iOS 7 to set a left indent after a bullet point?

Thanks in advance,

Frank



Solution 1:[1]

Below it the code I use to set a bulleted paragraph. This comes straight out of a working app and is used to apply the style to the entire paragraph in response to a user clicking on a formatting button. I have tried to put in all the dependent methods but may have missed some.

Note that I am setting most indents in centimetres and hence the use of the conversion functions at the end of the listing.

I am also checking for the presence of a tab character (no tab key on iOS!) and automatically insert a dash and a tab.

If all you need is the paragraph style then look at the last few methods below where the firstLineIndent etc get set up.

Note that these calls all get wrapped in [textStorage beginEditing/endEditing]. Despite the (IBAction) below the method is not getting called by a UI object directly.

        - (IBAction) styleBullet1:(id)sender
        {
            NSRange charRange = [self rangeForUserParagraphAttributeChange];
            NSTextStorage *myTextStorage = [self textStorage];

            // Check for "-\t" at beginning of string and add if not found
            NSAttributedString *attrString = [myTextStorage attributedSubstringFromRange:charRange];
            NSString *string = [attrString string];

            if ([string rangeOfString:@"\t"].location == NSNotFound) {
                NSLog(@"string does not contain tab so insert one");
                NSAttributedString * aStr = [[NSAttributedString alloc] initWithString:@"-\t"];
                // Insert a bullet and tab
                [[self textStorage] insertAttributedString:aStr atIndex:charRange.location];

            } else {
                NSLog(@"string contains tab");
            }

            if ([self isEditable] && charRange.location != NSNotFound)
            {
                [myTextStorage setAttributes:[self bullet1Style] range:charRange];
            }
        }

        - (NSDictionary*)bullet1Style
        {
            return [self createStyle:[self getBullet1ParagraphStyle] font:[self normalFont] fontColor:[UIColor blackColor] underlineStyle:NSUnderlineStyleNone];

        }

        - (NSDictionary*)createStyle:(NSParagraphStyle*)paraStyle font:(UIFont*)font fontColor:(UIColor*)color underlineStyle:(int)underlineStyle
        {
            NSMutableDictionary *style = [[NSMutableDictionary alloc] init];
            [style setValue:paraStyle forKey:NSParagraphStyleAttributeName];
            [style setValue:font forKey:NSFontAttributeName];
            [style setValue:color forKey:NSForegroundColorAttributeName];
            [style setValue:[NSNumber numberWithInt: underlineStyle] forKey:NSUnderlineStyleAttributeName];

            FLOG(@" font is %@", font);

            return style;
        }

        - (NSParagraphStyle*)getBullet1ParagraphStyle
        {
            NSMutableParagraphStyle *para;
            para = [self getDefaultParagraphStyle];
            NSMutableArray *tabs = [[NSMutableArray alloc] init];
            [tabs addObject:[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft location:[self ptsFromCMF:1.0] options:nil]];
            //[tabs addObject:[[NSTextTab alloc] initWithType:NSLeftTabStopType location:[self ptsFromCMF:1.0]]];
            [para setTabStops:tabs];
            [para setDefaultTabInterval:[self ptsFromCMF:2.0]];
            [para setFirstLineHeadIndent:[self ptsFromCMF:0.0]];
            //[para setHeaderLevel:0];
            [para setHeadIndent:[self ptsFromCMF:1.0]];
            [para setParagraphSpacing:3];
            [para setParagraphSpacingBefore:3];
            return para;
        }
    - (NSMutableParagraphStyle*)getDefaultParagraphStyle
    {
        NSMutableParagraphStyle *para;
        para = [[NSParagraphStyle defaultParagraphStyle]mutableCopy];
        [para setTabStops:nil];
        [para setAlignment:NSTextAlignmentLeft];
        [para setBaseWritingDirection:NSWritingDirectionLeftToRight];
        [para setDefaultTabInterval:[self ptsFromCMF:3.0]];
        [para setFirstLineHeadIndent:0];
        //[para setHeaderLevel:0];
        [para setHeadIndent:0.0];
        [para setHyphenationFactor:0.0];
        [para setLineBreakMode:NSLineBreakByWordWrapping];
        [para setLineHeightMultiple:1.0];
        [para setLineSpacing:0.0];
        [para setMaximumLineHeight:0];
        [para setMinimumLineHeight:0];
        [para setParagraphSpacing:6];
        [para setParagraphSpacingBefore:3];
        //[para setTabStops:<#(NSArray *)#>];
        [para setTailIndent:0.0];
        return para;
    }
-(NSNumber*)ptsFromCMN:(float)cm
{
    return [NSNumber numberWithFloat:[self ptsFromCMF:cm]];
}
-(float)ptsFromCMF:(float)cm
{
    return cm * 28.3464567;
}

Solution 2:[2]

So I've looked around, and here is the extracted minimal code from Duncan's answer to make it work:

NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:yourLabel.text];

NSMutableParagraphStyle *paragrahStyle = [[NSMutableParagraphStyle alloc] init];
[paragrahStyle setParagraphSpacing:4];
[paragrahStyle setParagraphSpacingBefore:3];
[paragrahStyle setFirstLineHeadIndent:0.0f];  // First line is the one with bullet point
[paragrahStyle setHeadIndent:10.5f];    // Set the indent for given bullet character and size font

[attributedString addAttribute:NSParagraphStyleAttributeName value:paragrahStyle
                         range:NSMakeRange(0, [self.descriptionLabel.text length])];

yourLabel.attributedText = attributedString;

And here is the result of that in my app:

Quadratic Master

Solution 3:[3]

This is the easiest solution I've found:

let bulletList = UILabel()
let bulletListArray = ["line 1 - enter a bunch of lorem ipsum here so it wraps to the next line", "line 2", "line 3"]
let joiner = "\n"

var paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.headIndent = 10
paragraphStyle.firstLineHeadIndent = 0

let attributes = [NSParagraphStyleAttributeName: paragraphStyle]
let bulletListString = joiner.join(bulletListArray.map { "• \($0)" })

bulletList.attributedText = NSAttributedString(string: bulletListString, attributes: attributes)

the theory being each string in the array acts like a 'paragraph' and the paragraph style gets 0 indent on the first line which gets a bullet added using the map method.. then for every line after it gets a 10 px indent (adjust spacing for your font metrics)

Solution 4:[4]

Other answers rely on setting the indent size with a constant value. That means you'll have to manually update it if you're changing fonts, and will not work well if you're using Dynamic Type. Fortunately, measuring text is easy.

Let's say you have some text and some attributes:

NSString *text = @"• Some bulleted paragraph";
UIFont *font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
NSDictionary *attributes = @{NSFontAttributeName: font};

Here's how to measure the bullet and create a paragraph style accordingly:

NSString *bulletPrefix = @"• ";
CGSize size = [bulletPrefix sizeWithAttributes:attributes];
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.headIndent = size.width;

We insert this in our attributes and create the attributed string:

NSMutableDictionary *indentedAttributes = [attributes mutableCopy];
indentedAttributes[NSParagraphStyleAttributeName] = [paragraphStyle copy];
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:text attributes:indentedAttributes];

Solution 5:[5]

Swift 5

I made an extension for NSAttributedString that adds a convenience initializer which properly indents different types of lists.

extension NSAttributedString {

    convenience init(listString string: String, withFont font: UIFont) {
        self.init(attributedListString: NSAttributedString(string: string), withFont: font)
    }

    convenience init(attributedListString attributedString: NSAttributedString, withFont font: UIFont) {
        guard let regex = try? NSRegularExpression(pattern: "^(\\d+\\.|[•\\-\\*])(\\s+).+$",
                                                   options: [.anchorsMatchLines]) else { fatalError() }
        let matches = regex.matches(in: attributedString.string, options: [],
                                    range: NSRange(location: 0, length: attributedString.string.utf16.count))
        let nsString = attributedString.string as NSString
        let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)

        for match in matches {
            let size = NSAttributedString(
                string: nsString.substring(with: match.range(at: 1)) + nsString.substring(with: match.range(at: 2)),
                attributes: [.font: font]).size()
            let indentation = ceil(size.width)
            let range = match.range(at: 0)

            let paragraphStyle = NSMutableParagraphStyle()

            if let style = attributedString.attribute(.paragraphStyle, at: 0, longestEffectiveRange: nil, in: range)
                as? NSParagraphStyle {
                paragraphStyle.setParagraphStyle(style)
            }

            paragraphStyle.tabStops = [NSTextTab(textAlignment: .left, location: indentation, options: [:])]
            paragraphStyle.defaultTabInterval = indentation
            paragraphStyle.firstLineHeadIndent = 0
            paragraphStyle.headIndent = indentation

            mutableAttributedString.addAttribute(.font, value: font, range: range)
            mutableAttributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: range)
        }

        self.init(attributedString: mutableAttributedString)
    }
}

Example usage: How the convenience initializer is used

The number of spaces after each bullet etc. doesn't matter. The code will calculate the appropriate indentation width dynamically based on how many tabs or spaces you decide to have after your bullet.

If the attributed string already has a paragraph style, the convenience initializer will retain the options of that paragraph style and apply some options of its own.

Supported symbols: •, -, *, numbers followed by a period (e.g. 8.)

Solution 6:[6]

You all can do this simple thing using Attributes Inspector,Select Indent Field and do whatever changes you wanted to do :)

enter image description here

Solution 7:[7]

Based off thisispete's solution, updated to Swift 4.2.

Swift 4.2

let array = ["1st", "2nd", "3rd"]
let textView = UITextView()
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.firstLineHeadIndent = 0
paragraphStyle.headIndent = 12
let bulletListText = array.map { "• \($0)" }.joined(separator: "\n")
let attributes = [
    NSAttributedString.Key.paragraphStyle: paragraphStyle,
    NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17.0)
]
textView.attributedText = NSAttributedString(string: bulletListText, attributes: attributes)

Solution 8:[8]

I made a swift solution (Swift 2.3 at the moment) based on Lukas implementation. I had a little issue with the lines that had no bullet points, so I made the extension so you can optionally pass a range to apply the paragraph style.

extension String{

    func getAllignedBulletPointsMutableString(bulletPointsRange: NSRange = NSMakeRange(0, 0)) -> NSMutableAttributedString{

        let attributedString: NSMutableAttributedString = NSMutableAttributedString(string: self)
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.paragraphSpacing = 0
        paragraphStyle.paragraphSpacingBefore = 0
        paragraphStyle.firstLineHeadIndent = 0
        paragraphStyle.headIndent = 7.5

        attributedString.addAttributes([NSParagraphStyleAttributeName: paragraphStyle], range: bulletPointsRange)
        return attributedString
    }

}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Lukas Petr
Solution 3 phatmann
Solution 4 skagedal
Solution 5
Solution 6 Infaz
Solution 7 Jayden Irwin
Solution 8