Split Attributed String and Retain Formatting - ios

How can you take an existing NSAttributedString and divide it based on a predefined separator while maintaining formatting? It does not seem that componentsSeparatedByString will operate on an NSAttributedString.
My current workaround produces the splits at the correct points, but only outputs an NSString. Thus losing formatting.
NSData *rtfFileData = [NSData dataWithContentsOfFile:path];
NSAttributedString *rtfFileAttributedString = [[NSAttributedString alloc] initWithData:rtfFileData options:#{NSDocumentTypeDocumentAttribute:NSRTFTextDocumentType} documentAttributes:nil error:nil];
NSString *rtfFileString = [rtfFileAttributedString string];
NSString *importSeparator = #"###";
// Wish I could do this
// NSArray *separatedArray = [rtfFileAttributedString componentsSeparatedByString:importSeparatorPref];
NSArray *separatedArray = [rtfFileString componentsSeparatedByString:importSeparatorPref];
NSLog( #"Separated array: %#", separatedArray );

You can make use of your split non-attributed string to split up the attributed string. One option would be:
NSData *rtfFileData = [NSData dataWithContentsOfFile:path];
NSAttributedString *rtfFileAttributedString = [[NSAttributedString alloc] initWithData:rtfFileData options:#{NSDocumentTypeDocumentAttribute:NSRTFTextDocumentType} documentAttributes:nil error:nil];
NSString *rtfFileString = [rtfFileAttributedString string];
NSString *importSeparator = #"###";
NSArray *separatedArray = [rtfFileString componentsSeparatedByString:importSeparatorPref];
NSMutableArray *separatedAttributedArray = [NSMutableArray arrayWithCapacity:separatedArray.count];
NSInteger start = 0;
for (NSString *sub in separatedArray) {
NSRange range = NSMakeRange(start, sub.length);
NSAttributedString *str = [rtfFileAttributedString attributedSubstringFromRange:range];
[separatedAttributedArray addObject:str];
start += range.length + importSeparator.length;
}
NSLog(#"Separated attributed array: ", separatedAttributedArray);

In Swift 4, I made the function.
func splitAttributedString(inputString: NSAttributedString, seperateBy: String) -> [NSAttributedString] {
let input = inputString.string
let separatedInput = input.components(separatedBy: seperateBy)
var output = [NSAttributedString]()
var start = 0
for sub in separatedInput {
let range = NSMakeRange(start, sub.utf16.count)
let attribStr = inputString.attributedSubstring(from: range)
output.append(attribStr)
start += range.length + seperateBy.count
}
return output
}

Here is an NSAttributedString extension that works in a similar fashion to some of the other examples here.
private extension NSAttributedString {
func components(separatedBy separator: String) -> [NSAttributedString] {
var result = [NSAttributedString]()
let separatedStrings = string.components(separatedBy: separator)
var range = NSRange(location: 0, length: 0)
for string in separatedStrings {
range.length = string.utf16.count
let attributedString = attributedSubstring(from: range)
result.append(attributedString)
range.location += range.length + separator.utf16.count
}
return result
}
}

In swift the answer is simple.
var string = NSAttributedString(
string: "This string is shorter than it should be for this questions answer.",
attributes: [.font: UIFont.systemFont(ofSize: 12)]
)
let range = NSRange(location: 0, length: 120)
let newString = string.attributedSubstring(from: range)
print(newString)

Related

Regular Expression and NSAttributedString to change colour of all the same words UILabel [duplicate]

There is a substring that occurs in a string several times. I use rangeOfString, but it seems that it can only find the first location. How can I find all the locations of the substring?
NSString *subString1 = #"</content>";
NSString *subString2 = #"--\n";
NSRange range1 = [newresults rangeOfString:subString1];
NSRange range2 = [newresults rangeOfString:subString2];
int location1 = range1.location;
int location2 = range2.location;
NSLog(#"%i",location1);
NSLog(#"%i",location2);
You can use rangeOfString:options:range: and set the third argument to be beyond the range of the first occurrence. For example, you can do something like this:
NSRange searchRange = NSMakeRange(0,string.length);
NSRange foundRange;
while (searchRange.location < string.length) {
searchRange.length = string.length-searchRange.location;
foundRange = [string rangeOfString:substring options:0 range:searchRange];
if (foundRange.location != NSNotFound) {
// found an occurrence of the substring! do stuff here
searchRange.location = foundRange.location+foundRange.length;
} else {
// no more substring to find
break;
}
}
Swift 3.0
Find all locations of substring i
let text = "This is the text and i want to replace something"
let mutableAttributedString = NSMutableAttributedString(string: text)
var searchRange = NSRange(location: 0, length: text.characters.count)
var foundRange = NSRange()
while searchRange.location < text.characters.count {
searchRange.length = text.characters.count - searchRange.location
foundRange = (text as NSString).range(of: "i", options: NSString.CompareOptions.caseInsensitive, range: searchRange)
if foundRange.location != NSNotFound {
// found an occurrence of the substring! do stuff here
searchRange.location = foundRange.location + foundRange.length
mutableAttributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: foundRange)
}
else {
// no more substring to find
break
}
}
//Apply
textLabel.attributedText = mutableAttributedString;
And this output-
This is my solution. Basically, the algorithm traverses the string looking for substring matches and returns those matches in an array.
Since an NSRange is a struct it cannot be added to the array directly. By using NSValue, I can encode the match first and then add it to the array. To retrieve the range, I then decode the NSValue object to an NSRange.
#import <Foundation/Foundation.h>
NSRange makeRangeFromIndex(NSUInteger index, NSUInteger length) {
return NSMakeRange(index, length - index);
}
NSArray<NSValue *> * allLocationsOfStringMatchingSubstring(NSString *text, NSString *pattern) {
NSMutableArray *matchingRanges = [NSMutableArray new];
NSUInteger textLength = text.length;
NSRange match = makeRangeFromIndex(0, textLength);
while(match.location != NSNotFound) {
match = [text rangeOfString:pattern options:0L range:match];
if (match.location != NSNotFound) {
NSValue *value = [NSValue value:&match withObjCType:#encode(NSRange)];
[matchingRanges addObject:value];
match = makeRangeFromIndex(match.location + 1, textLength);
}
}
return [matchingRanges copy];
}
int main(int argc, const char * argv[]) {
#autoreleasepool {
NSString *text = #"TATACCATGGGCCATCATCATCATCATCATCATCATCATCATCACAG";
NSString *pattern = #"CAT";
NSArray<NSValue *> *matches = allLocationsOfStringMatchingSubstring(text, pattern);
NSLog(#"Text: %#", text);
NSLog(#"Pattern: %#", pattern);
NSLog(#"Number of matches found: %li", matches.count);
[matches enumerateObjectsUsingBlock:^(NSValue *obj, NSUInteger idx, BOOL *stop) {
NSRange match;
[obj getValue:&match];
NSLog(#" Match found at index: %li", match.location);
}];
}
return 0;
}
Passing nil to [string rangeOfString:substring options:nil range:searchRange]; shows a warning.
To get rid of the warning, put in an enum from this group
enum {
NSCaseInsensitiveSearch = 1,
NSLiteralSearch = 2,
NSBackwardsSearch = 4,
NSAnchoredSearch = 8,
NSNumericSearch = 64,
NSDiacriticInsensitiveSearch = 128,
NSWidthInsensitiveSearch = 256,
NSForcedOrderingSearch = 512,
NSRegularExpressionSearch = 1024
};
https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSString_Class/index.html#//apple_ref/doc/constant_group/Search_and_Comparison_Options
Here is a version in Swift 2.2 of PengOne's answer with input from kevinlawler and Gibtang
Note: string and substring are of type NSString
let fullStringLength = (string as String).characters.count
var searchRange = NSMakeRange(0, fullStringLength)
while searchRange.location < fullStringLength {
searchRange.length = fullStringLength - searchRange.location
let foundRange = string.rangeOfString(substring as String, options: .CaseInsensitiveSearch, range: searchRange)
if foundRange.location != NSNotFound {
// found an occurrence of the substring! do stuff here
searchRange.location = foundRange.location + 1
} else {
// no more strings to find
break
}
}
I suggest using regular expression because it's a more declarative way and has fewer lines of code to write.
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"%#" options:nil error:nil];
NSString *toSearchStr = #"12312 %# Text %# asdsa %#";
__block int occurs = 0;
[regex enumerateMatchesInString:toSearchStr options:0 range:NSMakeRange(0, toSearchStr.length) usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) {
occurs++;
}];
// occurs == 3

Transform punctuations form half width to full width

I have a sentence below:
我今天去买菜,买了一个西瓜,花了1.2元,买了一个土豆,花了3.78元。还买了一个无花果,花了45.89,怎么办呢?好贵呀!贵的我不知道再买什么了。
The punctuations in it are half width. How to change them to fullwidth, like the following:
我今天去买菜,买了一个西瓜,花了1.2元,买了一个土豆,花了3.78元。还买了一个无花果,花了45.89,怎么办呢?好贵呀!贵的我不知道再买什么了。
Some punctuations to consider (not exhaustive):
, to ,
? to ?
! to !
"" to “”
; to ;
First, define the CharacterSet from which you want to transform your characters. So if you want only punctuation, the set could be CharacterSet.punctuationCharacters or CharacterSet.alphanumerics.inverted.
Then map each character from this set to its HalfwidthFullwidth transformation.
Swift 3 and 4
extension String {
func transformingHalfwidthFullwidth(from aSet: CharacterSet) -> String {
return String(characters.map {
if String($0).rangeOfCharacter(from: aSet) != nil {
let string = NSMutableString(string: String($0))
CFStringTransform(string, nil, kCFStringTransformFullwidthHalfwidth, true)
return String(string).characters.first!
} else {
return $0
}
})
}
}
Usage
let string = ",?!\"\";abc012図書館 助け 足場が痛い 多くの涙"
let result = string.transformingHalfwidthFullwidth(from: CharacterSet.alphanumerics.inverted)
// it prints: ,?!"";abc012図書館 助け 足場が痛い 多くの涙
print(result)
Objective-C
#implementation NSString (HalfwidthFullwidth)
- (NSString *)transformingHalfwidthFullwidth:(nonnull NSCharacterSet *)aSet {
NSUInteger len = self.length;
unichar buffer[len + 1];
[self getCharacters:buffer range:NSMakeRange(0, len)];
for (int i = 0; i < len; i++) {
unichar c = buffer[i];
NSMutableString *s = [[NSMutableString alloc] initWithCharacters:&c length:1];
NSRange r = [s rangeOfCharacterFromSet:aSet];
if (r.location != NSNotFound) {
CFStringTransform((CFMutableStringRef)s, nil, kCFStringTransformFullwidthHalfwidth, true);
[s getCharacters:buffer + i range:NSMakeRange(0, 1)];
}
}
return [NSString stringWithCharacters:buffer length:len];
}
#end
Usage
NSString *string = #",?!\"\";abc012図書館 助け 足場が痛い 多くの涙";
NSString *result = [string transformingHalfwidthFullwidth:NSCharacterSet.alphanumericCharacterSet.invertedSet];
// it prints: ,?!"";abc012図書館 助け 足場が痛い 多くの涙
NSLog(result);
you can use CFStringTransform like :
Objective C :
NSString *string = #" ? \"\"!我今天去买菜,买了一个西瓜,花了1.2元,买了一个土豆,花了3.78元。还买了一个无花果,花了45.89,怎么办呢?好贵呀!贵的我不知道再买什么了";
NSMutableString *convertedString = [string mutableCopy];
CFStringTransform((CFMutableStringRef)convertedString, NULL, kCFStringTransformFullwidthHalfwidth, true);
NSLog(#"%#",convertedString);
Swift 3.0 :
let string = NSMutableString( string: " ? \"\"!我今天去买菜,买了一个西瓜,花了1.2元,买了一个土豆,花了3.78元。还买了一个无花果,花了45.89,怎么办呢?好贵呀!贵的我不知道再买什么了" )
CFStringTransform( string, nil, kCFStringTransformFullwidthHalfwidth, true )
print(string)

ios Replace subStrings with a pattern

I have a very long string and it contains some information that I do not want. For example, it constantly has something like:
"<span class=/"phrasedesc/">....some text......</span phrasedesc>",
and I would like to remove all the substrings begin with "<span class=/"phrasedesc/">" and ends with "</span phrasedesc>", no matter what's between.
Any one can help me with this? Thank you.
Try this, it worked for me. :)
SWIFT:
let re = try! NSRegularExpression(pattern: "(.*)(《span class=\"phrasedesc\"》.*《\\/span phrasedesc》)(.*)", options: .AnchorsMatchLines)
let stringToMatch = "adfasdfasdf《span class=\"phrasedesc\"》ajshdfljkahsdkjf 《/span phrasedesc》asdfasdfasdfasdf"
let matches = re.matchesInString(stringToMatch, options: .WithTransparentBounds , range: NSRange(location: 0, length: stringToMatch.characters.count))
re.stringByReplacingMatchesInString(stringToMatch, options: .ReportCompletion, range: NSMakeRange(0, stringToMatch.characters.count), withTemplate: "$1$3")
OBJECTIVE - C:
NSString *stringToBeTrimmed = #"adfasdfasdf《span class=\"phrasedesc\"》ajshdfljkahsdkjf 《/span phrasedesc》asdfasdfasdfasdf";
NSString *pattern = #"(.*)(《span class=\"phrasedesc\"》.*《\\/span phrasedesc》)(.*)";
NSRegularExpression *expression = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionAnchorsMatchLines error:nil];
NSString * yourResultingString = [expression stringByReplacingMatchesInString:stringToBeTrimmed options:NSMatchingReportCompletion range:NSMakeRange(0, stringToBeTrimmed.length) withTemplate:#"$1$3"];
Consider using stringByReplacingOccurrencesOfString:
Swift:
let originalString = "<span class=\"phrasedesc\">....some text......</span phrasedesc>"
let newString = originalString.stringByReplacingOccurrencesOfString("<span class=\"phrasedesc\">", withString: "")
let newString = newString.stringByReplacingOccurrencesOfString("</span phrasedesc>", withString: "")
Objective-C:
NSString *originalString = #"<span class=\"phrasedesc\">....some text......</span phrasedesc>";
newString = [originalString stringByReplacingOccurrencesOfString:#"<span class=\"phrasedesc\">" withString:#""];
newString = [newString stringByReplacingOccurrencesOfString:#"</span phrasedesc>" withString:#""];
The result will be newString == "....some text......"

replace entire text string in NSAttributedString without modifying other attributes

I have a reference to NSAttributedString and i want to change the text of the attributed string.
I guess i have to created a new NSAttributedString and update the reference with this new string. However when i do this i lose the attributed of previous string.
NSAttributedString *newString = [[NSAttributedString alloc] initWithString:text];
[self setAttributedText:newString];
I have reference to old attributed string in self.attributedText. How can i retain the previous attributed in the new string?
You can use NSMutableAttributedString and just update the string, the attributes won't change.
Example:
NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:#"my string" attributes:#{NSForegroundColorAttributeName: [UIColor blueColor], NSFontAttributeName: [UIFont systemFontOfSize:20]}];
//update the string
[mutableAttributedString.mutableString setString:#"my new string"];
Swift
Change the text while keeping the attributes:
let myString = "my string"
let myAttributes = [NSAttributedString.Key.foregroundColor: UIColor.blue, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 40)]
let mutableAttributedString = NSMutableAttributedString(string: myString, attributes: myAttributes)
let myNewString = "my new string"
mutableAttributedString.mutableString.setString(myNewString)
The results for mutableAttributedString are
Notes
Any sub-ranges of attributes beyond index 0 are discarded. For example, if I add another attribute to the last word of the original string, it is lost after I change the string:
// additional attribute added before changing the text
let myRange = NSRange(location: 3, length: 6)
let anotherAttribute = [ NSAttributedString.Key.backgroundColor: UIColor.yellow ]
mutableAttributedString.addAttributes(anotherAttribute, range: myRange)
Results:
From this we can see that the new string gets whatever the attributes are at index 0 of the original string. Indeed, if we adjust the range to be
let myRange = NSRange(location: 0, length: 1)
we get
See also
my main answer about Swift attributed strings
I made a little extension to make this really easy:
import UIKit
extension UILabel {
func setTextWhileKeepingAttributes(string: String) {
if let newAttributedText = self.attributedText {
let mutableAttributedText = newAttributedText.mutableCopy()
mutableAttributedText.mutableString.setString(string)
self.attributedText = mutableAttributedText as? NSAttributedString
}
}
}
https://gist.github.com/wvdk/e8992e82b04e626a862dbb991e4cbe9c
This is the way using Objective-C (tested on iOS 9)
NSAttributedString *primaryString = ...;
NSString *newString = ...;
//copy the attributes
NSDictionary *attributes = [primaryString attributesAtIndex:0 effectiveRange:NSMakeRange(primaryString.length-1, primaryString.length)];
NSMutableAttributedString *newString = [[NSMutableAttributedString alloc] initWithString:newString attributes:attributes];
NSMutableAttributedString *primaryStringMutable = [[NSMutableAttributedString alloc] initWithAttributedString:primaryString];
//change the string
[primaryStringMutable setAttributedString::newString];
primaryString = [NSAttributedString alloc] initWithAttributedString:primaryStringMutable];
Check for the most important references: attributesAtIndex:effectiveRange: and setAttributedString:.
Darius answer is almost there. It contains a minor error. The correct is:
This is the way using Objective-C (tested on iOS 10)
NSAttributedString *primaryString = ...;
NSString *newString = ...;
//copy the attributes
NSRange range = NSMakeRange(primaryString.length-1, primaryString.length);
NSDictionary *attributes = [primaryString attributesAtIndex:0 effectiveRange:&range];
NSMutableAttributedString *newString = [[NSMutableAttributedString alloc] initWithString:newString attributes:attributes];
NSMutableAttributedString *primaryStringMutable = [[NSMutableAttributedString alloc] initWithAttributedString:primaryString];
//change the string
[primaryStringMutable setAttributedString::newString];
primaryString = [NSAttributedString alloc] initWithAttributedString:primaryStringMutable];
let mutableAttributedString = mySubTitleLabel.attributedText?.mutableCopy() as? NSMutableAttributedString
if let attrStr = mutableAttributedString{
attrStr.mutableString.setString("Inner space can be an example shown on the, third page of the tutorial.")
mySubTitleLabel.attributedText = attrStr;
}
I hope this code may help you, i have copied the attribute of the label to a mutableAttributedString and then set the string for it
For those of you working with UIButtons, here is an improved answer based on Wes's.
It seemed that updating a label of a button had better be done this way:
let newtext = "my new text"
myuibutton.setAttributedTitle(titlelabel.getTextWhileKeepingAttributes(string: newtext), for: .normal)
So I ended up with this extension:
import UIKit
extension UILabel {
func setTextWhileKeepingAttributes(string: String) {
if let newAttributedText = self.attributedText {
let mutableAttributedText = newAttributedText.mutableCopy()
(mutableAttributedText as AnyObject).mutableString.setString(string)
self.attributedText = mutableAttributedText as? NSAttributedString
}
}
func getTextWhileKeepingAttributes(string: String) -> NSAttributedString {
if let newAttributedText:NSAttributedString = self.attributedText {
let mutableAttributedText = newAttributedText.mutableCopy()
(mutableAttributedText as AnyObject).mutableString.setString(string)
return mutableAttributedText as! NSAttributedString
}
else {
// No attributes in this label, just create a new attributed string?
let attributedstring = NSAttributedString.init(string: string)
return attributedstring
}
}
}
Changing the text of a mutable string will not do the jobs, since it will only keep the attributes of the first character and apply this to all of the text. Which seems to be by design, since it is part of the documentation.
So if you want to copy all attributes or change the string, you need to copy all attributes manually. Then you can create a MutableAttributedString and change the text. Afterwards you apply all the attributes to the new MutableAttributedString.
I have done it this way for Xamarin (in C#), but I think you can easily understand it and adapt it for your language:
NSMutableAttributedString result = new
NSMutableAttributedString(attrStr.Value.Replace(blackSquare, bullet));
// You cannot simply replace an AttributedString's string, because that will discard attributes.
// Therefore, I will now copy all attributes manually to the new MutableAttributedString:
NSRange outRange = new NSRange(0, 0);
int attributeIndex = 0;
while (outRange.Location + outRange.Length < attrStr.Value.Length // last attribute range reached
&& attributeIndex < attrStr.Value.Length) // or last character reached
{
// Get all attributes for character at attributeIndex
var attributes = attrStr.GetAttributes(attributeIndex, out outRange);
if (attributes != null && attributes.Count > 0)
{
result.AddAttributes(attributes, outRange); // copy all found attributes to result
attributeIndex = (int)(outRange.Location + outRange.Length); // continue with the next range
}
else
{
attributeIndex++; // no attribues at the current attributeIndex, so continue with the next char
}
}
// all attributes are copied
None of the answers worked for me, but this one;
extension UILabel{
func setTextWhileKeepingAttributes(_ string: String) {
if let attributedText = self.attributedText {
let attributedString = NSMutableAttributedString(string: string,
attributes: [NSAttributedString.Key.font: font])
attributedText.enumerateAttribute(.font, in: NSRange(location: 0, length: attributedText.length)) { (value, range, stop) in
let attributes = attributedText.attributes(at: range.location, effectiveRange: nil)
attributedString.addAttributes(attributes, range: range)
}
self.attributedText = attributedString
}
}
}

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
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:
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;
}
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)
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];
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:
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.)
You all can do this simple thing using Attributes Inspector,Select Indent Field and do whatever changes you wanted to do :)
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)
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
}
}

Resources