Attribute the word within a specific range using NSRegularExpression in Swift - ios

I have some pure Html string and some of them has title tags <b>Title</b>.
facilities: "<b>Facilities</b><br/>24-hour security, Barbecue area, Car park, Clubhouse, Function room, Gym, Outdoor swimming pool, Playground, Swimming pool<br/><br/><b>Rooms</b><br/>Dining room, Ensuites, Living room, Maid\'s room, Utility room<br/><br/><b>Outdoor</b><br/>Balcony<br/><br/><b>View</b><br/>City, Open<br/><br/><b>Direction</b><br/>South East"
So i use NSRegularExpression pattern to extract titles from string and store in an array of strings. And later, i make these titles bold (attributed string) and display. So this is how i do that:
var titlesArray = [String]()
let regex = try! NSRegularExpression(pattern: "<b>(.*?)</b>", options: [])
let basicDescription = facilities as NSString
regex.enumerateMatchesInString(facilities, options: [], range: NSMakeRange(0, facilities.characters.count)) { result, flags, stop in
if let range = result?.rangeAtIndex(1) {
titlesArray.append(basicDescription.substringWithRange(range))
}
}
let convertedDescription = facilities.html2String as NSString
let attributedString = NSMutableAttributedString(string: convertedDescription as String, attributes: [NSFontAttributeName:UIFont.systemFontOfSize(14.0)])
let boldFontAttribute = [NSFontAttributeName: UIFont.boldSystemFontOfSize(15.0)]
if titlesArray.count > 0 {
for i in 0..<titlesArray.count {
attributedString.addAttributes(boldFontAttribute, range: convertedDescription.rangeOfString(titlesArray[i]))
}
}
So, everything is alright. But the problem is, sometimes i receive Html tagged strings which has duplicate words where one of them is title with title tag, another one is just a simple word which i do not need to bold. But this function will look for that word and bold it inside for loop and ignore the real title which comes after the simple word.
This is what i get:
So here, how can i ignore the first "Outdoor" and bold the second one which i want. Thank you for any help.

In Objective-C, shouldn't be too hard to translate in Swift (since it seems you already know some of the methods).
attr1 is rendered with init(data:, options:, documentAttributes:). I didn't add any other effects (like preferred size for bold/normal, color, you just have to enumerate on it and change the effects)
attr2 is rendered more the way you wanted with your regex. It just doesn't take in account all tags, just the bold, and I hardly coded the replacement for new lines (<br/> into \n). But that could be something to use. I didn't do more tests on your regex (the while loop could be stucked?)
NSString *str = #"<b>Facilities</b><br/>24-hour security, Barbecue area, Car park, Clubhouse, Function room, Gym, Outdoor swimming pool, Playground, Swimming pool<br/><br/><b>Rooms</b><br/>Dining room, Ensuites, Living room, Maid\'s room, Utility room<br/><br/><b>Outdoor</b><br/>Balcony<br/><br/><b>View</b><br/>City, Open<br/><br/><b>Direction</b><br/>South East";
NSError *errorAttr1 = nil;
NSAttributedString *attr1 = [[NSAttributedString alloc] initWithData:[str dataUsingEncoding:NSUTF8StringEncoding] options:#{NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType} documentAttributes:nil error:&errorAttr1];
if (errorAttr1)
{
NSLog(#"Error AttributedStr Conversion with initWithData:options:documentsAttributes:error: %#", errorAttr1);
}
else
{
NSLog(#"attr1: %#", attr1);
[_tv1 setAttributedText:attr1];
}
str = [str stringByReplacingOccurrencesOfString:#"<br/>" withString:#"\n"];
NSError *errorRegex = nil;
NSString *openingTag = #"<b>";
NSString *closingTag = #"</b>";
NSString *pattern = [NSString stringWithFormat:#"%#(.*?)%#", openingTag, closingTag];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&errorRegex];
if (errorRegex)
{
NSLog(#"Error regex: %#", errorRegex);
return;
}
NSDictionary *boldAttributes = #{NSForegroundColorAttributeName:[UIColor darkGrayColor],
NSFontAttributeName:[UIFont boldSystemFontOfSize:15]};
NSDictionary *normalAttributes = #{NSForegroundColorAttributeName:[UIColor darkGrayColor],
NSFontAttributeName:[UIFont systemFontOfSize:14]};
NSMutableAttributedString *attr2 = [[NSMutableAttributedString alloc] initWithString:str attributes:normalAttributes]; //Add the initial attributes there
//Now we'll add the specific attribues
NSTextCheckingResult *match = [regex firstMatchInString:[attr2 string] options:0 range:NSMakeRange(0, [attr2 length])];
while (match)
{
NSRange range = [match range];
NSString *foundStr = [[attr2 string] substringWithRange:range];
NSAttributedString *temp = [[NSAttributedString alloc] initWithString:[foundStr substringWithRange:NSMakeRange([openingTag length], [foundStr length]-[openingTag length]-[closingTag length])] attributes:boldAttributes];
[attr2 replaceCharactersInRange:range withAttributedString:temp];
match = [regex firstMatchInString:[attr2 string] options:0 range:NSMakeRange(0, [attr2 length])];
}
NSLog(#"attr2: %#", attr2);
[_tv2 setAttributedText:attr2];
_tv1 and _tv2 are two UITextView (IBOulet).
It rendered: (_tv1 is the one on top, _tv2 is the second one).

I dont know what exactly you are trying to do, but did tou consider using an initializer that takes HTML?
An example code for Playgrounds would be:
if let url = Bundle.main.url(forResource: "file", withExtension: "html") {
do {
let data = try Data(contentsOf: url)
let attributedString = try NSAttributedString(data: data, options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType], documentAttributes: nil)
let labelRect = CGRect(x: 0, y: 0, width: 500, height: 250)
let label = UILabel(frame: labelRect)
label.numberOfLines = 2
label.attributedText = attributedString
} catch {
print(error.localizedDescription)
}
}
With this HTML:
<style>
body {
font-size: 20;
font-family: sans-serif;
}
</style>
<p><b>Hello</b> world</p>
<p>Good morning!</p>
The output looks like:

Related

Easiest way to style the text of an UILabel in iOS7/iOS8

I'm learning objective c a little bit to write an iPad app. I've mostly done some html5/php projects and learned some python at university. But one thing that really blows my mind is how hard it is to just style some text in an objective C label.
Maybe I'm coming from a lazy markdown generation, but really, if I want to let an UILabel look like:
Objective: Construct an equilateral triangle from the line segment AB.
In markdown this is as simple as:
**Objective:** Construct an *equilateral* triangle from the line segment AB.
Is there really no pain free objective C way to do this ? All the tutorials I read really wanted me to write like 15 lines of code. For something as simple as this.
So my question is, what is the easiest way to do this, if you have a lot of styling to do in your app ? Will styling text become more natural with swift in iOS8 ?
You can use NSAttributedString's data:options:documentAttributes:error: initializer (first available in iOS 7.0 SDK).
import UIKit
let htmlString = "<b>Objective</b>: Construct an <i>equilateral</i> triangle from the line segment AB."
let htmlData = htmlString.dataUsingEncoding(NSUTF8StringEncoding)
let options = [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType]
var error : NSError? = nil
let attributedString = NSAttributedString(data: htmlData, options: options, documentAttributes: nil, error: &error)
if error == nil {
// we're good
}
Note: You might also want to include NSDefaultAttributesDocumentAttribute option in the options dictionary to provide additional global styling (such as telling not to use Times New Roman).
Take a look into NSAttributedString UIKit Additions Reference for more information.
I faced similar frustrations while trying to use attributed text in Xcode, so I feel your pain. You can definitely use multiple NSMutableAttributedtext's to get the job done, but this is very rigid.
UIFont *normalFont = [UIFont fontWithName:#"..." size:20];
UIFont *boldFont = [UIFont fontWithName:#"..." size:20];
UIFont *italicizedFont = [UIFont fontWithName:#"..." size:20];
NSMutableAttributedString *total = [[NSMutableAttributedString alloc]init];
NSAttributedString *string1 = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"Objective"] attributes:#{NSFontAttributeName:boldFont}];
NSAttributedString *string2 = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#": Construct an "] attributes:#{NSFontAttributeName:normalFont}];
NSAttributedString *string3 = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"equilateral "] attributes:#{NSFontAttributeName:italicizedFont}];
NSAttributedString *string4 = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"triangle from the line segment AB."] attributes:#{NSFontAttributeName:normalFont}];
[total appendAttributedString:string1];
[total appendAttributedString:string2];
[total appendAttributedString:string3];
[total appendAttributedString:string4];
[self.someLabel setAttributedText: total];
Another option is to use NSRegularExpression. While this will require more lines of code, it is a more fluid way of bolding, changing color, etc from an entire string at once. For your purposes however, using the appendAttributedString will be the shortest way with a label.
UIFont *normalFont = [UIFont fontWithName:#"..." size:20];
UIFont *boldFont = [UIFont fontWithFamilyName:#"..." size: 20];
UIFont *italicizedFont = [UIFont fontWithFamilyName:#"..." size: 20];
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat: #"Objective: Construct an equilateral triangle from the line segment AB."] attributes:#{NSFontAttributeName:normalFont}];
NSError *regexError;
NSRegularExpression *regex1 = [NSRegularExpression regularExpressionWithPattern:#"Objective"
options:NSRegularExpressionCaseInsensitive error:&regexError];
NSRegularExpression *regex2 = [NSRegularExpression regularExpressionWithPattern:#"equilateral"
options:NSRegularExpressionCaseInsensitive error:&regexError];
if (!regexError)
{
NSArray *matches1 = [regex1 matchesInString:[attributedString string]
options:0
range:NSMakeRange(0, [[attributedString string] length])];
NSArray *matches2 = [regex2 matchesInString:[attributedString string]
options:0
range:NSMakeRange(0, [[attributedString string] length])];
for (NSTextCheckingResult *aMatch in matches1)
{
NSRange matchRange = [aMatch range];
[attributedString setAttributes:#{NSFontAttributeName:boldFont}
range:matchRange];
}
for (NSTextCheckingResult *aMatch in matches2)
{
NSRange matchRange = [aMatch range];
[attributedString setAttributes:#{NSFontAttributeName:italicizedFont}
range:matchRange];
}
[self.someLabel setAttributedText: attributedString];
Just to update the akashivskyy’s answer (+1) with contemporary Swift syntax:
guard let data = htmlString.data(using: .utf8) else { return }
do {
let attributedString = try NSAttributedString(
data: data,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil
)
...
} catch {
print(error)
}

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
}
}

Change attributes of substrings in a NSAttributedString

This question may be a duplicate of this one. But the answers don't work for me and I want to be more specific.
I have a NSString, but I need a NS(Mutable)AttributedString and some of the words in this string should be given a different color. I tried this:
NSString *text = #"This is the text and i want to replace something";
NSDictionary *attributes = # {NSForegroundColorAttributeName : [UIColor redColor]};
NSMutableAttributedString *subString = [[NSMutableAttributedString alloc] initWithString:#"AND" attributes:attributes];
NSMutableAttributedString *newText = [[NSMutableAttributedString alloc] initWithString:text];
newText = [[newText mutableString] stringByReplacingOccurrencesOfString:#"and" withString:[subString mutableString]];
The "and" should be uppercase an red.
The documentation says that mutableString keeps the attribute mappings. But with my replacing-thing, I have no more attributedString on the right side of the assignment (in the last line of my code-snippet).
How can I get what I want? ;)
#Hyperlord's answer will work, but only if there is one occurence of the word "and" in the input string. Anyway, what I would do is use NSString's stringByReplacingOccurrencesOfString: initially to change every "and" to an "AND", then use a little regex to detect matches in attributed string, and apply NSForegroundColorAttributeName at that range. Here's an example:
NSString *initial = #"This is the text and i want to replace something and stuff and stuff";
NSString *text = [initial stringByReplacingOccurrencesOfString:#"and" withString:#"AND"];
NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:text];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"(AND)" options:kNilOptions error:nil];
NSRange range = NSMakeRange(0,text.length);
[regex enumerateMatchesInString:text options:kNilOptions range:range usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSRange subStringRange = [result rangeAtIndex:1];
[mutableAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:subStringRange];
}];
And finally, just apply the attributed string to your label.
[myLabel setAttributedText:mutableAttributedString];
I think you should create a NSMutableAttributedString using the existing NSString and then add the style attributes with the appropriate NSRange in order to colorize the parts you want to emphasize for example:
NSString *text = #"This is the text and i want to replace something";
NSMutableAttributedString *mutable = [[NSMutableAttributedString alloc] initWithString:text];
[mutable addAttribute: NSForegroundColorAttributeName value:[UIColor redColor] range:[text rangeOfString:#"and"]];
Be aware: this is just from my head and not tested at all ;-)
Please try this code in Swift 2
var someStr = "This is the text and i want to replace something"
someStr.replaceRange(someStr.rangeOfString("and")!, with: "AND")
let attributeStr = NSMutableAttributedString(string: someStr)
attributeStr.setAttributes([NSForegroundColorAttributeName: UIColor.yellowColor()], range: NSMakeRange(17, 3) )
testLbl.attributedText = attributeStr
Here's another implementation (in Swift) that's helpful if you're doing some more complex manipulations (such as adding/deleting characters) with your attributed string:
let text = "This is the text and i want to replace something"
let mutAttrStr = NSMutableAttributedString(string: text)
let pattern = "\\band\\b"
let regex = NSRegularExpression(pattern: pattern, options: .allZeros, error: nil)
while let result = regex!.firstMatchInString(mutAttrStr.string, options: .allZeros, range:NSMakeRange(0, count(mutAttrStr.string)) {
let substring = NSMutableAttributedString(attributedString: mutAttrStr.attributedSubstringFromRange(result.range))
// manipulate substring attributes here
substring.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range NSMakeRange(0, count(substring.string))
mutAttrStr.replaceCharactersInRange(result.range, withAttributedString: substring)
}
Your final attributed string should be:
let finalAttrStr = mutAttrStr.copy() as! NSAttributedString

Replace UITextViews text with attributed string

I have a UITextView and when the user is entering text into it, I want to format the text on the fly. Something like syntax highlighting...
For that I'd like to use UITextView...
Everything works fine expect one problem: I take the text from the text view and make an NSAttributedString from of it. I make some edits to this attributed string and set it back as the textView.attributedText.
This happens everytime the user types. So I have to remember the selectedTextRange before the edit to the attributedText and set it back afterwards so that the user can continue typing at the place he was typing before. The only problem is that once the text is long enough to require scrolling, the UITextView will now start scrolling to the top if I type slowly.
Here is some sample code:
- (void)formatTextInTextView:(UITextView *)textView
{
NSRange selectedRange = textView.selectedRange;
NSString *text = textView.text;
// This will give me an attributedString with the base text-style
NSMutableAttributedString *attributedString = [self attributedStringFromString:text];
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"#(\\w+)" options:0 error:&error];
NSArray *matches = [regex matchesInString:text
options:0
range:NSMakeRange(0, text.length)];
for (NSTextCheckingResult *match in matches)
{
NSRange matchRange = [match rangeAtIndex:0];
[attributedString addAttribute:NSForegroundColorAttributeName
value:[UIColor redColor]
range:matchRange];
}
textView.attributedText = attributedString;
textView.selectedRange = selectedRange;
}
Is there any solution without using CoreText directly? I like the UITextViews ability to select text and so on....
I am not sure that this is correct solution, but it works.
Just disable scrolling before formatting text and enable it after formatting
- (void)formatTextInTextView:(UITextView *)textView
{
textView.scrollEnabled = NO;
NSRange selectedRange = textView.selectedRange;
NSString *text = textView.text;
// This will give me an attributedString with the base text-style
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text];
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"#(\\w+)" options:0 error:&error];
NSArray *matches = [regex matchesInString:text
options:0
range:NSMakeRange(0, text.length)];
for (NSTextCheckingResult *match in matches)
{
NSRange matchRange = [match rangeAtIndex:0];
[attributedString addAttribute:NSForegroundColorAttributeName
value:[UIColor redColor]
range:matchRange];
}
textView.attributedText = attributedString;
textView.selectedRange = selectedRange;
textView.scrollEnabled = YES;
}
Used Sergeys's answer myself and ported it to Swift 2:
func formatTextInTextView(textView: UITextView) {
textView.scrollEnabled = false
let selectedRange = textView.selectedRange
let text = textView.text
// This will give me an attributedString with the base text-style
let attributedString = NSMutableAttributedString(string: text)
let regex = try? NSRegularExpression(pattern: "#(\\w+)", options: [])
let matches = regex!.matchesInString(text, options: [], range: NSMakeRange(0, text.characters.count))
for match in matches {
let matchRange = match.rangeAtIndex(0)
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: matchRange)
}
textView.attributedText = attributedString
textView.selectedRange = selectedRange
textView.scrollEnabled = true
}
Swift 2.0:
let myDisplayTxt:String = "Test String"
let string: NSMutableAttributedString = NSMutableAttributedString(string: self.myDisplayTxt)
string.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(0, 5))
string.addAttribute(String(kCTForegroundColorAttributeName), value: UIColor.redColor().CGColor as AnyObject, range: NSMakeRange(0, 5))
self.sampleTextView.attributedText = string
In Swift 4:
func createAttributedText() {
let stringText = "Test String"
let stringCount = stringText.count
let string: NSMutableAttributedString = NSMutableAttributedString(string: stringText)
string.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: NSMakeRange(0, stringCount))
self.textView.attributedText = string
}

How can I extract a URL from a sentence that is in a NSString?

What I'm trying to accomplish is as follows. I have a NSString with a sentence that has a URL within the sentience. I'm needing to be able to grab the URL that is presented within any sentence that is within a NSString so for example:
Let's say I had this NSString
NSString *someString = #"This is a sample of a http://example.com/efg.php?EFAei687e3EsA sentence with a URL within it.";
I need to be able to extract http://example.com/efg.php?EFAei687e3EsA from within that NSString. This NSString isn't static and will be changing structure and the url will not necessarily be in the same spot of the sentence. I've tried to look into the three20 code but it makes no sense to me. How else can this be done?
Use an NSDataDetector:
NSString *string = #"This is a sample of a http://example.com/efg.php?EFAei687e3EsA sentence with a URL within it.";
NSDataDetector *linkDetector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink error:nil];
NSArray *matches = [linkDetector matchesInString:string options:0 range:NSMakeRange(0, [string length])];
for (NSTextCheckingResult *match in matches) {
if ([match resultType] == NSTextCheckingTypeLink) {
NSURL *url = [match URL];
NSLog(#"found URL: %#", url);
}
}
This way you don't have to rely on an unreliable regular expression, and as Apple upgrades their link detection code, you get those improvements for free.
Edit: I'm going to go out on a limb here and say you should probably use NSDataDetector as Dave mentions. Far less prone to error than regular expressions.
Take a look at regular expressions. You can construct a simple one to extract the URL using the NSRegularExpression class, or find one online that you can use. For a tutorial on using the class, see here.
The code you want essentially looks like this (using John Gruber's super URL regex):
NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:#"(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}/)(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))" options:NSRegularExpressionCaseInsensitive error:NULL];
NSString *someString = #"This is a sample of a http://example.com/efg.php?EFAei687e3EsA sentence with a URL within it.";
NSString *match = [someString substringWithRange:[expression rangeOfFirstMatchInString:someString options:NSMatchingCompleted range:NSMakeRange(0, [someString length])]];
NSLog(#"%#", match); // Correctly prints 'http://example.com/efg.php?EFAei687e3EsA'
That will extract the first URL in any string (of course, this does no error checking, so if the string really doesn't contain any URL's it won't work, but take a look at the NSRegularExpression class to see how to get around it.
Use Like This:
NSError *error = nil;
NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink
error:&error];
[detector enumerateMatchesInString:someString
options:0
range:NSMakeRange(0, someString.length)
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop)
{
if (result.resultType == NSTextCheckingTypeLink)
{
NSString *str = [NSString stringWithFormat:#"%#",result.URL];
NSLOG(%#,str);
}
}];
This will Output the all links in your someString one by one
Swift 2 :
let input = "This is a test with the URL https://www.hackingwithswift.com to be detected."
let detector = try! NSDataDetector(types: NSTextCheckingType.Link.rawValue)
let matches = detector.matchesInString(input, options: [], range: NSMakeRange(0, input.characters.count))
for match in matches {
let url = (input as NSString).substringWithRange(match.range)
print(url)
}
Source
use this:
NSURL *url;
NSArray *listItems = [someString componentsSeparatedByString:#" "];
for(int i=0;i<[listItems count];i++)
{
NSString *str=[listItems objectAtIndex:i];
if ([str rangeOfString:#"http://"].location == NSNotFound)
NSLog(#"Not url");
else
url=[NSURL URLWithString:str];
}
you need two things:
A category that adds regex to NSString (i.e. RegexKit)
Matching Regex for URLS.
regards,
Funny you mention three20, that was the first place I was going to go look for the answer. Here's the method from three20:
- (void)parseURLs:(NSString*)string {
NSInteger index = 0;
while (index < string.length) {
NSRange searchRange = NSMakeRange(index, string.length - index);
NSRange startRange = [string rangeOfString:#"http://" options:NSCaseInsensitiveSearch
range:searchRange];
if (startRange.location == NSNotFound) {
NSString* text = [string substringWithRange:searchRange];
TTStyledTextNode* node = [[[TTStyledTextNode alloc] initWithText:text] autorelease];
[self addNode:node];
break;
} else {
NSRange beforeRange = NSMakeRange(searchRange.location, startRange.location - searchRange.location);
if (beforeRange.length) {
NSString* text = [string substringWithRange:beforeRange];
TTStyledTextNode* node = [[[TTStyledTextNode alloc] initWithText:text] autorelease];
[self addNode:node];
}
NSRange searchRange = NSMakeRange(startRange.location, string.length - startRange.location);
NSRange endRange = [string rangeOfString:#" " options:NSCaseInsensitiveSearch
range:searchRange];
if (endRange.location == NSNotFound) {
NSString* URL = [string substringWithRange:searchRange];
TTStyledLinkNode* node = [[[TTStyledLinkNode alloc] initWithText:URL] autorelease];
node.URL = URL;
[self addNode:node];
break;
} else {
NSRange URLRange = NSMakeRange(startRange.location,
endRange.location - startRange.location);
NSString* URL = [string substringWithRange:URLRange];
TTStyledLinkNode* node = [[[TTStyledLinkNode alloc] initWithText:URL] autorelease];
node.URL = URL;
[self addNode:node];
index = endRange.location;
}
}
}
}
Every time it does [self addNode:node]; after the first if part, it's adding a found URL. This should get you started! Hope this helps. :)
Using Swift 2.2 - NSDataDetector
let string = "here is the link www.google.com"
let types: NSTextCheckingType = [ .Link]
let detector = try? NSDataDetector(types: types.rawValue)
detector?.enumerateMatchesInString(string, options: [], range: NSMakeRange(0, (string as NSString).length)) { (result, flags, _) in
if(result?.URL != nil){
print(result?.URL)
}
}
Swift 4.x
Xcode 12.x
let string = "This is a test with the URL https://www.hackingwithswift.com to be detected. www.example.com"
let types: NSTextCheckingResult.CheckingType = [ .link]
let detector = try? NSDataDetector(types: types.rawValue)
detector?.enumerateMatches(in: string, options: [], range: NSMakeRange(0, (string as NSString).length)) { (result, flags, _) in
if(result?.url != nil){
print(result?.url)
}
}

Resources