How can I concatenate NSAttributedStrings? - ios

I need to search some strings and set some attributes prior to merging the strings, so having NSStrings -> Concatenate them -> Make NSAttributedString is not an option, is there any way to concatenate attributedString to another attributedString?

I'd recommend you use a single mutable attributed string a #Linuxios suggested, and here's another example of that:
NSMutableAttributedString *mutableAttString = [[NSMutableAttributedString alloc] init];
NSString *plainString = // ...
NSDictionary *attributes = // ... a dictionary with your attributes.
NSAttributedString *newAttString = [[NSAttributedString alloc] initWithString:plainString attributes:attributes];
[mutableAttString appendAttributedString:newAttString];
However, just for the sake of getting all the options out there, you could also create a single mutable attributed string, made from a formatted NSString containing the input strings already put together. You could then use addAttributes: range: to add the attributes after the fact to the ranges containing the input strings. I recommend the former way though.

If you're using Swift, you can just overload the + operator so that you can concatenate them in the same way you concatenate normal strings:
// concatenate attributed strings
func + (left: NSAttributedString, right: NSAttributedString) -> NSAttributedString
{
let result = NSMutableAttributedString()
result.append(left)
result.append(right)
return result
}
Now you can concatenate them just by adding them:
let helloworld = NSAttributedString(string: "Hello ") + NSAttributedString(string: "World")

Swift 3: Simply create a NSMutableAttributedString and append the attributed strings to them.
let mutableAttributedString = NSMutableAttributedString()
let boldAttribute = [
NSFontAttributeName: UIFont(name: "GothamPro-Medium", size: 13)!,
NSForegroundColorAttributeName: Constants.defaultBlackColor
]
let regularAttribute = [
NSFontAttributeName: UIFont(name: "Gotham Pro", size: 13)!,
NSForegroundColorAttributeName: Constants.defaultBlackColor
]
let boldAttributedString = NSAttributedString(string: "Warning: ", attributes: boldAttribute)
let regularAttributedString = NSAttributedString(string: "All tasks within this project will be deleted. If you're sure you want to delete all tasks and this project, type DELETE to confirm.", attributes: regularAttribute)
mutableAttributedString.append(boldAttributedString)
mutableAttributedString.append(regularAttributedString)
descriptionTextView.attributedText = mutableAttributedString
swift5 upd:
let captionAttribute = [
NSAttributedString.Key.font: Font.captionsRegular,
NSAttributedString.Key.foregroundColor: UIColor.appGray
]

Try this:
NSMutableAttributedString* result = [astring1 mutableCopy];
[result appendAttributedString:astring2];
Where astring1 and astring2 are NSAttributedStrings.

2020 | SWIFT 5.1:
You're able to add 2 NSMutableAttributedString by the following way:
let concatenated = NSAttrStr1.append(NSAttrStr2)
Another way works with NSMutableAttributedString and NSAttributedString both:
[NSAttrStr1, NSAttrStr2].joinWith(separator: "")
Another way is....
var full = NSAttrStr1 + NSAttrStr2 + NSAttrStr3
and:
var full = NSMutableAttributedString(string: "hello ")
// NSAttrStr1 == 1
full += NSAttrStr1 // "hello 1"
full += " world" // "hello 1 world"
You can do this with the following extension:
// works with NSAttributedString and NSMutableAttributedString!
public extension NSAttributedString {
static func + (left: NSAttributedString, right: NSAttributedString) -> NSAttributedString {
let leftCopy = NSMutableAttributedString(attributedString: left)
leftCopy.append(right)
return leftCopy
}
static func + (left: NSAttributedString, right: String) -> NSAttributedString {
let leftCopy = NSMutableAttributedString(attributedString: left)
let rightAttr = NSMutableAttributedString(string: right)
leftCopy.append(rightAttr)
return leftCopy
}
static func + (left: String, right: NSAttributedString) -> NSAttributedString {
let leftAttr = NSMutableAttributedString(string: left)
leftAttr.append(right)
return leftAttr
}
}
public extension NSMutableAttributedString {
static func += (left: NSMutableAttributedString, right: String) -> NSMutableAttributedString {
let rightAttr = NSMutableAttributedString(string: right)
left.append(rightAttr)
return left
}
static func += (left: NSMutableAttributedString, right: NSAttributedString) -> NSMutableAttributedString {
left.append(right)
return left
}
}

If you're using Cocoapods, an alternative to both above answers that let you avoid mutability in your own code is to use the excellent NSAttributedString+CCLFormat category on NSAttributedStrings that lets you write something like:
NSAttributedString *first = ...;
NSAttributedString *second = ...;
NSAttributedString *combined = [NSAttributedString attributedStringWithFormat:#"%#%#", first, second];
It of course it just uses NSMutableAttributedString under the covers.
It also has the extra advantage of being a fully fledged formatting function — so it can do a lot more than appending strings together.

// Immutable approach
// class method
+ (NSAttributedString *)stringByAppendingString:(NSAttributedString *)append toString:(NSAttributedString *)string {
NSMutableAttributedString *result = [string mutableCopy];
[result appendAttributedString:append];
NSAttributedString *copy = [result copy];
return copy;
}
//Instance method
- (NSAttributedString *)stringByAppendingString:(NSAttributedString *)append {
NSMutableAttributedString *result = [self mutableCopy];
[result appendAttributedString:append];
NSAttributedString *copy = [result copy];
return copy;
}

You can try SwiftyFormat
It uses following syntax
let format = "#{{user}} mentioned you in a comment. #{{comment}}"
let message = NSAttributedString(format: format,
attributes: commonAttributes,
mapping: ["user": attributedName, "comment": attributedComment])

private var fillAttributes:[NSMutableAttributedString.Key : Any]? = nil
fontAttributes = [.foregroundColor : SKColor.red,
.strokeWidth : 0.0,
.font : CPFont(name: "Verdana-Bold",
.size : 50,]
fontAttributes.updateValue(SKColor.green, forKey: .foregroundColor)

Related

Load text in Punjabi language in iOS

I have received response from back-end in the below mentioned form, I want to display content in Punjabi, Gurmukhi Punjabi and English language simultaneously,
{
"AK_Index" = "APNE SEVAK KEE AAPE RAKE";
Ang = 747;
Author = "Guru Arjan Dev Ji";
Bani = "<null>";
"Bani_ID" = "<null>";
English = "Guru Nanak has met the Supreme Lord God; I am a sacrifice to Your Feet. ||4||1||47||";
"English_Initials" = gnmptckb;
Gurmukhi = "guru nwnku imilAw pwrbRhmu qyirAw crxw kau bilhwrw ]4]1]47]";
"Gurmukhi_Initials" = "gnmpqckb]";
ID = 32133;
Kirtan = Kirtan;
"Kirtan_ID" = 2853;
Punjabi = "gurU nwnk Awpxy prm pRBU nUM iml ipAw hY Aqy aus dy pYrW qoN GolI jWdw hY[";
Raag = "Raag Soohee";
"Teeka_Arth" = "hy pRBU! mYN qyry crnW qoN sdky jWdw hW[ ijs mnu`K nUM gurU nwnk iml ipAw, aus nUM prmwqmw iml ipAw ]4]1]47]";
"Teeka_Pad_Arth" = "kau = nUM, qoN ]4]1]47]";
}
Attached is screenshot what the result would look like. The screenshot is from android. I want exact same result in iOS.
How to achieve this in iOS.
Any help will be appreciated.
I have created an extension to change color font of some particular string.
extension NSMutableAttributedString {
func setFontColorAndSize(_ textToFind : String,withColor color: UIColor,andFont font :UIFont){
let range = self.mutableString.range(of: textToFind, options: .caseInsensitive)
if range.location != NSNotFound {
setAttributes([NSAttributedStringKey.font : font,NSAttributedStringKey.foregroundColor :color], range: range)
}
}
// This is how you change
let resultString = NSMutableAttributedString(string: "hello") // convert any string to mutable attributed
resultString.setFontColorAndSize("hel", withColor: UIColor.grey, andFont: UIFont.boldSystemFont(ofSize: 17)) // pass the string you want to set particular color and font
UILabel.attributedText = resultString // than update the label
There is nothing much you can do on the mobile end. You can ask server to send UTF-8 encoded data instead of plain English string.
English = UTF-8 encoded English string data//data for hello
Gurmukhi = UTF-8 encoded Punjabi string data //data for ਪਜੌਬਹਗਕਤਚਜ
Now you can create the string to display in that language as:-
let englishString = String(data: English, encoding: .utf8)
let punjabiSring = String(data: Gurmukhi, encoding: .utf8)
Initializer is
init?(data: Data, encoding: String.Encoding)
Finally got it working using "Gurblipi.ttf" font which internally changes the English text to Punjabi language.
let attributedString = NSMutableAttributedString()
for shabadDetail in shabadDetailList {
attributedString.append(NSMutableAttributedString(string: shabadDetail.gurmukhi, attributes: [NSAttributedStringKey.font : FontHelper.guruLippi(fontsize: textview.font!.pointSize), NSAttributedStringKey.foregroundColor: TextFormatting.gurbaniColor]))
attributedString.append(NSAttributedString(string: TextFormatting.doubleLineBreak))
}
textview.attributedText = attributedString
textview.textAlignment = .center
No encoding is required from native or back-end.

swift: edit text in JSON

I have JSON file with text and label. In JSON file I want to add this characters <b> or <i> to some words. For example:
"this is my text <b>bold<b> or <i>italic<i>"
And before when I show text in label I want to analyse my text and if some words have this characters <b> or <i> I want to make this word bold or italic in the text of the label. How to do it? Or there are another way to make bold text from JSON?
UPDATE:
Yours code:
/* Set Tagged Text & Fonts */
let taggedTextString = myText.text
let tagFont = UIFont.boldSystemFont(ofSize: 17.0)
let normalFont = UIFont.systemFont(ofSize: 17.0)
/* You can simply assign the AttributedText to the modified String, which may return nil. */
textLabel.attributedText = taggedTextString.modifyFontWithTags(openingTag: "<tag>", closingTag: "</tag>", taggedFont: tagFont, unTaggedFont: normalFont)
My code:
var attributedString = NSMutableAttributedString(string: myText.text)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 5
attributedString.addAttribute(NSAttributedStringKey.paragraphStyle, value:paragraphStyle, range:NSMakeRange(0, attributedString.length))
textLabel.attributedText = attributedString;
Is this code the best way to union your and my code?
for i in stride(from: 0, to: allTagsSplitArray.count, by: 1) {
finalAttrStr.append(NSMutableAttributedString(string: allTagsSplitArray[i], attributes: [kCTFontAttributeName as NSAttributedStringKey : i % 2 == 0 ? untaggedTextFont : taggedTextFont]))
// my code
finalAttrStr.addAttribute(NSAttributedStringKey.paragraphStyle, value:paragraphStyle, range:NSMakeRange(0, finalAttrStr.length))
}
The simplest way I seem to have accomplished this is by running through the string and scanning for your key tags - <b>, <i>, etc.
To make things easier (and more reusable), we should make an extension of our String or NSAttributedString classes. For this example, I've extended the String class, and and I return a NSAttributedString:
func modifyFontWithTags(openingTag: String, closingTag: String, taggedFont: UIFont?, unTaggedFont: UIFont?) -> NSMutableAttributedString? {
/* Make sure we have everything we need. */
guard let taggedTextFont = taggedFont,
let untaggedTextFont = unTaggedFont,
!self.isEmpty,
self.contains(openingTag),
self.contains(closingTag) else { return nil }
/* Split the string up using our closing tag. */
let closingTagSplitArray = self.components(separatedBy: closingTag)
/* Make a placeholder array. */
var allTagsSplitArray = [String]()
/* Iterate through our split array. */
for item in closingTagSplitArray {
if item.contains(openingTag) {
/* Strip the opening tag & append. */
allTagsSplitArray.append(contentsOf: item.components(separatedBy: openingTag))
} else {
/* Just append. */
allTagsSplitArray.append(item)
}
}
/* Instantiate our attributed string. */
let finalAttrStr = NSMutableAttributedString()
for i in stride(from: 0, to: allTagsSplitArray.count, by: 1) {
/* Add our font to every-other item in the array (the tagged portions). */
finalAttrStr.append(NSMutableAttributedString(string: allTagsSplitArray[i], attributes: [NSFontAttributeName : i % 2 == 0 ? untaggedTextFont : taggedTextFont]))
}
return finalAttrStr
}
Note: In this example, I take in an UIFont parameter, but you can modify this function to take in whatever attribute types you want.

Syntax-highlighting Text With a Custom List of Keywords

I'm working on a macOS application. I need to syntax-highlight text that is placed over TextView (NSTextView) with a list of selected words. For simplicity, I'm actually testing the same feature on the iPhone Simulator. Anyway, a list of words to highlight comes as a form of an array. The following is what I have.
func HighlightText {
let tagArray = ["let","var","case"]
let style = NSParagraphStyle.defaultParagraphStyle().mutableCopy() as! NSMutableParagraphStyle
style.alignment = NSTextAlignment.Left
let words = textView.string!.componentsSeparatedByString(" ") // textView.text (UITextView) or textView.string (NSTextView)
let attStr = NSMutableAttributedString()
for i in 0..<words.count {
let word = words[i]
if HasElements.containsElements(tagArray,text: word,ignore: true) {
let attr = [
NSForegroundColorAttributeName: syntaxcolor,
NSParagraphStyleAttributeName: style,
]
let str = (i != words.count-1) ? NSAttributedString(string: word.stringByAppendingString(" "), attributes: attr) : NSAttributedString(string: word, attributes: attr)
attStr.appendAttributedString(str)
} else {
let attr = [
NSForegroundColorAttributeName: NSColor.blackColor(),
NSParagraphStyleAttributeName: style,
]
let str = (i != words.count-1) ? NSAttributedString(string: word.stringByAppendingString(" "), attributes: attr) : NSAttributedString(string: word, attributes: attr)
attStr.appendAttributedString(str)
}
}
textView.textStorage?.setAttributedString(attStr)
}
class HasElements {
static func containsElements(array:Array<String>,text:String,ignore:Bool) -> Bool {
var has = false
for str in array {
if str == text {
has = true
}
}
return has
}
}
The simple methodology here is to separate the entire string of text into words with a white space (" ") and puts each word in an array (words). The containsElements function simply tells whether or not the selected word contains one of the keywords in the array (tagArray). If it returns true, the word is put in an NSMutableAttributedString with a highlight color. Otherwise, it's put in the same attributed string with a plain color.
The problem with this simple methodology is that a separated word puts the last word and /n and the next word together. For example, if I have a string like
let base = 3
let power = 10
var answer = 1
, only the first 'let' will be highlighted as the code puts 3 and the next let together like '3\nlet.' If I separate any word containing \n with a fast enumeration, the code won't detect each new paragraph well. I appreciate any advice to make it better. Just FYI, I'm going to leave this topic open to both macOS and iOS.
Muchos thankos
Couple different options. String has a function called componentsSeparatedByCharactersInSet that allows you to separate by a character set you define. Unfortunately this won't work since you want to separate by \n which is more than one character.
You could split the words twice.
let firstSplit = textView.text!.componentsSeparatedByString(" ")
var words = [String]()
for word in firstSplit {
let secondSplit = word.componentsSeparatedByString("\n")
words.appendContentsOf(secondSplit)
}
But then you wouldn't have any sense of the line breaks.. You'd need to re add them back in.
Finally, the easiest hack is simply:
let newString = textView.text!.stringByReplacingOccurrencesOfString("\n", withString: "\n ")
let words = newString.componentsSeparatedByString(" ")
So basically you add your own spaces.

Completion block in Swift from Objective C

I'm having trouble translating this over to Swift. Any help is GREATLY appreciated, thank you!
[segControl setTitleFormatter:^NSAttributedString *(LBCSegmentedControl *segmentedControl, NSString *title, NSUInteger index, BOOL selected) {
NSAttributedString *attString = [[NSAttributedString alloc] initWithString:title attributes:#{NSForegroundColorAttributeName : [UIColor blueColor]}];
return attString;
}];
You only use title in the block so may as well replace the other 3 parameters with _, meaning you don't care about them. Give this a try:
segControl.tittleFormater = {_, title, _, _ -> NSAttributedString in
NSAttributedString(string: title, attributes:[
NSForegroundColorAttributeName: UIColor.blueColor
])
}
Or a longer version, if the compiler complains about ambigous data types:
segControl.tittleFormater = {(segmentedControl: LBCSegmentedControl, title: NSString, index: Int, selected: Bool) -> NSAttributedString in
NSAttributedString(string: title, attributes:[
NSForegroundColorAttributeName: UIColor.blueColor
])
}
It depends upon how the setTitleFormatter was implemented. If it's just a method, you'd do something like:
segControl.setTitleFormatter { (segmentedControl, title, index, selected) -> NSAttributedString! in
return NSAttributedString(string: title, attributes: [NSForegroundColorAttributeName : UIColor.blueColor()])
}
If it was defined as a block property, you'd do something like:
segControl.titleFormatter = { (segmentedControl, title, index, selected) -> NSAttributedString! in
return NSAttributedString(string: title, attributes: [NSForegroundColorAttributeName : UIColor.blueColor()])
}
Both of the above assume the Objective-C class lacks nullability annotations. If it has been audited for nullability, then some of those ! will either be ? or not needed at all.

Are there any analogues of [NSString stringWithFormat:] for NSAttributedString

Usually I build app interface in interface builder. Sometimes design requires to use attributed strings (fonts, colors and etc.). It's easy to configure if string is static.
But if string is dynamic (format with arguments) then there are no ways to configure attributes in interface builder. It requires to write a lot of code.
I am looking for some analogues of [NSString stringWithFormat:] for NSAttributedString. So I will be able to set string format and necessary attributes in interface builder, and then provide necessary arguments in code.
For example:
Let's consider that I need display string with such format: "%d + %d = %d" (all numbers are bold).
I want to configure this format in interface builder. In code I want to provide arguments: 1, 1, 2. App should show "1 + 1 = 2".
Compatible with Swift 4.2
public extension NSAttributedString {
convenience init(format: NSAttributedString, args: NSAttributedString...) {
let mutableNSAttributedString = NSMutableAttributedString(attributedString: format)
args.forEach { (attributedString) in
let range = NSString(string: mutableNSAttributedString.string).range(of: "%#")
mutableNSAttributedString.replaceCharacters(in: range, with: attributedString)
}
self.init(attributedString: mutableNSAttributedString)
}
}
Usage:
let content = NSAttributedString(string: "The quick brown %# jumps over the lazy %#")
let fox = NSAttributedString(string: "fox", attributes: [.font: Fonts.CalibreReact.boldItalic.font(size: 40)])
let dog = NSAttributedString(string: "dog", attributes: [.font: Fonts.CalibreReact.lightItalic.font(size: 11)])
attributedLabel.attributedText = NSAttributedString(format: content, args: fox, dog)
Result:
I was looking for good existing solution for this question, but no success.
So I was able to implement it on my own.
That's why I am self-answering the question to share the knowledge with community.
Solution
NSAttributedString+VPAttributedFormat category provides methods for building attributed string based on attributed format and arguments that should satisfy this format.
The most suitable case of using this category is text controls with variable attributed text configured in interface builder.
You need set correct string format to attributed text and configure necessary attributes.
Then you need pass necessary arguments in code by using methods of this category.
Format syntax is the same as in [NSString stringWithFormat:] method;
Can be used in Objective C and Swift code;
Requires iOS 6.0 and later;
Integrated with CocoaPods;
Covered with unit tests.
Usage
1. Import framework header or module
// Objective C
// By header
#import <VPAttributedFormat/VPAttributedFormat.h>
// By module
#import VPAttributedFormat;
// Swift
import VPAttributedFormat
2. Set correct format and attributes for text control in interface builder
3. Create IBOutlet and link it with text control
// Objective C
#property (nonatomic, weak) IBOutlet UILabel *textLabel;
// Swift
#IBOutlet weak var textLabel: UILabel!
4. Populate format with necessary arguments
// Objective C
NSString *hot = #"Hot";
NSString *cold = #"Cold";
self.textLabel.attributedText = [NSAttributedString vp_attributedStringWithAttributedFormat:self.textLabel.attributedText,
hot,
cold];
// Swift
let hot = "Hot"
let cold = "Cold"
var arguments: [CVarArgType] = [hot, cold]
textLabel.attributedText = withVaList(arguments) { pointer in
NSAttributedString.vp_attributedStringWithAttributedFormat(textLabel.attributedText, arguments: pointer)
}
5. See result
Examples
VPAttributedFormatExample is an example project. It provides Basic and Pro format examples.
Here's a category I wrote to add the method to NSAttributedString. You'll have to pass in NULL as the last argument to the function however, otherwise it will crash to the va_list restrictions on detecting size. [attributedString stringWithFormat:attrFormat, attrArg1, attrArg2, NULL];
#implementation NSAttributedString(stringWithFormat)
+(NSAttributedString*)stringWithFormat:(NSAttributedString*)format, ...{
va_list args;
va_start(args, format);
NSMutableAttributedString *mutableAttributedString = (NSMutableAttributedString*)[format mutableCopy];
NSString *mutableString = [mutableAttributedString string];
while (true) {
NSAttributedString *arg = va_arg(args, NSAttributedString*);
if (!arg) {
break;
}
NSRange rangeOfStringToBeReplaced = [mutableString rangeOfString:#"%#"];
[mutableAttributedString replaceCharactersInRange:rangeOfStringToBeReplaced withAttributedString:arg];
}
va_end(args);
return mutableAttributedString;
}
#end
Here is a Swift 4 extension based on TheJeff's answer (corrected for multiple substitutions). It is restricted to substituting placeholders with NSAttributedString's:
public extension NSAttributedString {
convenience init(format: NSAttributedString, args: NSAttributedString...) {
let mutableNSAttributedString = NSMutableAttributedString(attributedString: format)
var nsRange = NSString(string: mutableNSAttributedString.string).range(of: "%#")
var param = 0
while nsRange.location != NSNotFound {
guard args.count > 0, param < args.count else {
fatalError("Not enough arguments provided for \(format)")
}
mutableNSAttributedString.replaceCharacters(in: nsRange, with: args[param])
param += 1
nsRange = NSString(string: mutableNSAttributedString.string).range(of: "%#")
}
self.init(attributedString: mutableNSAttributedString)
}
}

Resources