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

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

Related

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.

Swift Checking if entire word is present in a string [duplicate]

This question already has answers here:
Detect whole word in NSStrings
(4 answers)
Closed 6 years ago.
What I want to do is to check if an entire word is present in a string. Let me give you an example.
let mainString = "this is a my string"
let searchString = "str"
if mainString.containsString(searchString) {
}
Here this condition will be true but I do not want this. I want it to be true when either "this" or "is" or "a" or "my" or "string" is searched in the mainString meaning I want to compare the whole word not the characters within the string. I hope I have elaborated it.
// The following method will return a string without punctuation and non-required stuff characters
Source of information : How to remove special characters from string in Swift 2?
func removeSpecialCharsFromString(mainString) -> String {
let okayChars : Set<Character> =
Set("abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLKMNOPQRSTUVWXYZ".characters)
return String(text.characters.filter {okayChars.contains($0) })
}
let stringWithoutSpecialChars = removeSpecialCharsFromString(mainString)
// Source of info : Chris's answer on this page
let components = stringWithoutSpecialChars.componentsSeparatedByString(" ")
for component in components {
print(component)
}
You have to use a Regex like that :
NSString *pattern = #"\\str\\b";
NSRange range = [text rangeOfString:pattern options:NSRegularExpressionSearch|NSCaseInsensitiveSearch];
The \b at the end of the pattern do what you want, it will match only whole words.
I would recommend splitting your string into components and seeing if a component matches your string. Below is a method that uses components to check if any word matches a given search term.
func isTermInString(term:String, stringToSearch:String) -> Bool {
let components = stringToSearch.componentsSeparatedByString(" ")
for component in components {
if component == term {
return true
}
}
return false
}

Escaping Strings in Swift

I am going to create a CSV file programmatically and would like to properly escape my strings before writing them.
I assume I'll need to escape commas and probably need to surround each value in single or double quotes (and thus will need to escape those too). Plus any carriage return / new line constants.
I was going to write it all myself but then found this in Objective-C and said why not just convert it, as it looks quite thorough:
-(NSString *)escapeString:(NSString *)s
{
NSString * escapedString = s;
BOOL containsSeperator = !NSEqualRanges([s rangeOfString:#","], NSMakeRange(NSNotFound, 0));
BOOL containsQuotes = !NSEqualRanges([s rangeOfString:#"\""], NSMakeRange(NSNotFound, 0));
BOOL containsLineBreak = !NSEqualRanges([s rangeOfString:#"\n"], NSMakeRange(NSNotFound, 0));
if (containsQuotes) {
escapedString = [escapedString stringByReplacingOccurrencesOfString:#"\"" withString:#"\"\""];
}
if (containsSeperator || containsLineBreak) {
escapedString = [NSString stringWithFormat:#"\"%#\"", escapedString];
}
return escapedString;
}
Before I go and convert this, however, I wanted to ask the community if there is an easier way now that we're in Swift 2. Have any interesting/new changes occurred for strings that I might want to consider in favor of "Swiftifying" the above code? I did some Googling but nothing jumped out at me and I want to really make sure I do a good job here. :-)
Thanks!
You could reduce your code and save it as a String extension:
extension String {
func escapeString() -> String {
var newString = self.stringByReplacingOccurrencesOfString("\"", withString: "\"\"")
if newString.containsString(",") || newString.containsString("\n") {
newString = String(format: "\"%#\"", newString)
}
return newString
}
}
Also few tests:
var test1 = String("Test")
test1.escapeString() // produces Test
var test2 = String("Test\n")
test2.escapeString() // produces "Test\n"
var test3 = String("Test, Test2")
test3.escapeString() // produces "Test, Test2"

IOS insert line break to data retrieved from parse.com

i am trying to retrieve some string data from parse.com and i've added "\n, \n & \r" into the data, but it still does not give me the line breaks, how do format the line below to get line breaks.
self.label.text = [NSString stringWithFormat:#"%#", [object objectForKey:#"ParseStringDate"]];
if you get the text that should be presented in multiple lines then you should try to set for your label this property:
self.label.numberOfLines = 0;
it basically means that label can be multiline, make sure that frame of that label is sufficient to show more that one line of text
if some has the same challenge in swift - please find attached a short string extension:
extension String {
func formatStringFromParseWithLineBreaks() -> String {
let sourceString: String = self
let resultString = sourceString.stringByReplacingOccurrencesOfString("\\n", withString: "\n")
return resultString
}
}
It is easy to use:
let stringFromParse = "Great\\nExample"
let newString = stringFromParse.formatStringFromParseWithLineBreaks()
Have fun
self.label.text = [NSString stringWithFormat:#"first\n%#\n another", [object objectForKey:#"ParseStringDate"]];
Make sure you have numberOfLines of the label set to 0. This should work.

How can I concatenate NSAttributedStrings?

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)

Resources