Unable to render an NSAttributedString as a 2 column tabbed bullet list in a PDF - ios

I am constructing a large string that is output into a PDF file, but right now, I'd like to have a 2 column, bulleted list in my document. However, I have yet to figure out the correct settings that will allow me to get the desired tabbing effect.
Currently, I am testing the following code:
let mutableString = NSMutableAttributedString()
let words = ["this", "is", "really", "getting", "old"]
let paragraphStyle = NSMutableParagraphStyle()
var tabStops = [NSTextTab]()
let tabInterval: CGFloat = 250.0
for index in 0..<12 {
tabStops.append(NSTextTab(textAlignment: .left,
location: tabInterval * CGFloat(index),
options: [:]))
}
paragraphStyle.tabStops = tabStops
for index in 0..<words.count {
if index != 0 && index % 2 == 0 {
mutableString.append(NSAttributedString(string: "\n"))
}
if index % 2 == 1 {
let attributedText = NSAttributedString(string: "\t", attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle])
mutableString.append(attributedText)
}
let word = words[index]
let attributedString = NSMutableAttributedString(string: "\u{2022} \(word)",
attributes: [:])
mutableString.append(attributedString)
}
When I feed this into my PDF generator, it produces the following result:
Ultimately, I want "is" and "getting" to be aligned with the middle of the document, so that I can accommodate much larger words.

It turns out that I was in the ballpark, but definitely not close.
The following provides the desired split column effect:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.tabStops = [
// 274 would be the midpoint of my document
NSTextTab(textAlignment: .left, location: 274, options: [:])
]
let string = "\u{2022} This\t\u{2022} is\n\u{2022} getting\t\u{2022} really\n\u{2022} old"
let attributedString = NSAttributedString(
string: string,
attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle]
)
For bonus points, should you want to have multiple columns in your document, the following will accomplish this (pardon my crude formatting):
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.tabStops = [
NSTextTab(textAlignment: .left, location: 100, options: [:]),
NSTextTab(textAlignment: .left, location: 300, options: [:])
]
let string = "\u{2022} This\t\u{2022} is\t\u{2022} getting\n\u{2022} really\t\u{2022} old"
let attributedString = NSAttributedString(
string: string,
attributes: [NSAttributedString.Key.paragraphStyle: paragraphStyle]
)
And will look like this:
What is going on here?
So, what I learned here is that the tabStops tells iOS what location within the line to place the tab:
The first tab will go to position 100
The second tab will go to position 300
A third tab will wrap around the document and go to position 100 as well
Regarding tabbing, if you assign a tab with location 0 in the first index, then tabbing to a newline will get it aligned with the left edge.
As to what fixed the issue for me. I was relying on an approach where each component of the string was added as it was encountered. However, this string would fail to format properly. Instead, by merging everything into a single string and applying the attributes seen in my working code, I was able to get it to align properly.
I also tested using the individual components as seen in my question, but with the paragraph style attributes applied as well, and that resulted in a working solution as well.
Based on this, it appears that my mistake was to mix strings that had, and did not have, the desired tabbing behavior.

Related

Center 3 Uneven Labels - Interface Builder

Attached is an image of what I'm trying to center and the attempts I have made using XCode 11.2.1
How do I center the 3 labels of different sizes (the data for the number will be dynamic also) horizontally in the view?
Things I have tried to do:
I tried putting a horizontal constraint on the LateBEDView (which contains the 3 labels), but when you view it on the Simulator or Actual Device it puts all the text to the right of the center. I have tried using spacers (empty views) on both sides but can't figure out what the settings should be? Any help is greatly appreciated!
Here's how I would do it. I'd make this one label, which after all is easily centered. Okay, I'll assume you know how to do that.
Then I'd use an attributed string to create the different parts of the string, including the "subscripting". So, first I'd extend the attributed string keys to include the three parts:
extension NSAttributedString.Key {
static let part1 = NSAttributedString.Key(rawValue: "part1")
static let part2 = NSAttributedString.Key(rawValue: "part2")
static let part3 = NSAttributedString.Key(rawValue: "part3")
}
Then I'd set the label's attributed text:
let mas = NSMutableAttributedString()
mas.append(NSAttributedString(string: "Late BED = ", attributes: [
.font:UIFont.systemFont(ofSize: 15),
.foregroundColor:UIColor.black,
.part1:"part1"
]))
mas.append(NSAttributedString(string: "20.0", attributes: [
.font:UIFont.systemFont(ofSize: 15),
.foregroundColor:UIColor.black,
.part2:"part2"
]))
mas.append(NSAttributedString(string: " Gy(3.0)", attributes: [
.font:UIFont.systemFont(ofSize: 9),
.foregroundColor:UIColor.black,
.baselineOffset:-10,
.part3:"part3"
]))
self.label.attributedText = mas
The result looks like what you've got, and of course you can tweak it as needed:
Okay, and here's the really clever part. Because I demarcated the three parts of the attributed string, I can find and change the text of any of them, at will. As you say, the data for each number needs to be able to change. That's why you used three labels! But now I'm showing how to do that with one label.
For example, let's say I want to change "20.0" to "30.4". That is your second label, and my .part2. Here's how you do it:
let s = self.label.attributedText
// skip past part 1 and find part 2
var r = NSRange()
let mas = s?.mutableCopy() as! NSMutableAttributedString
let _ = mas.attribute(.part2, at: 0, longestEffectiveRange: &r,
in: NSRange(location: 0, length: 100))
// find range of part 2
let _ = mas.attribute(.part2, at: r.length, longestEffectiveRange: &r,
in: NSRange(location: 0, length: 100))
mas.replaceCharacters(in: r, with: "30.4")
self.label.attributedText = mas

How to set a style for a specific word inside UITextView?

I have a UITextView in which I am trying to style a particular word. The problem I am facing is that on setting a style for the word, it's also applying the style to all the other occurrences of the word. I just want one particular instance of the word say first or third to have the custom style.
Consider the text present inside UITextView.
Sunset is the time of day when our sky meets the outer space solar winds.
There are blue, pink, and purple swirls, spinning and twisting, like clouds of balloons caught in
a whirlwind. The sun moves slowly to hide behind the line of horizon, while the
moon races to take its place in prominence atop the night sky. People slow to a crawl,
entranced, fully forgetting the deeds that must still be done. There is a coolness, a
calmness, when the sun does set.
If I set the style to sun then both the occurrences of the word is getting the style applied.
Here is the code
let normalAttr = [NSAttributedString.Key.font: UIFont(name: "Oswald", size: 19.0), NSAttributedString.Key.paragraphStyle : style]
let customAttr = [NSAttributedString.Key.font: UIFont(name: "Oswald", size: 19.0), NSAttributedString.Key.foregroundColor: UIColor.red]
let words = textView.text.components(separatedBy: " ")
let newText = NSMutableAttributedString()
for word in words {
if (word == selectedWord) {
newText.append(NSMutableAttributedString(string: word + " " , attributes: selectedAttributes as [NSAttributedString.Key : Any]))
} else {
newText.append(NSMutableAttributedString(string:word + " ", attributes: normalAttributes as [NSAttributedString.Key : Any]))
}
}
textView.attributedText = newText
I just want to apply the style to one word any help on how could I do that?
How are you choosing which instance to replace?
The simplest way to do this would be to just maintain your own counter:
var counter = 0
for word in words {
if (word == selectedWord) {
counter += 1
// myTarget being the first or third or whatever
let attributesToUse = (counter == myTarget) ? selectedAttributes : normalAttributes
newText.append(NSMutableAttributedString(string: word + " " , attributes: attributesToUse as [NSAttributedString.Key : Any]))
} else {
newText.append(NSMutableAttributedString(string:word + " ", attributes: normalAttributes as [NSAttributedString.Key : Any]))
}
}
But you can certainly get cleaner by using NSAttributedStrings and looking for the range of your text..
let myText = NSMutableAttributedString(string: textView.text, attributes: normalAttributes)
// this will only turn up the FIRST occurrence
if let range = myText.range(of: selectedWord) {
let rangeOfSelected = NSRange(range, in: myText)
myText.setAttributes(selectedAttributes, range: rangeOfSelected)
}
If you want to use arbitrary occurrence you can prob write an extension that creates an array of all the ranges and then pick the one that matters, this is a good reference for that: https://medium.com/#weijentu/find-and-return-the-ranges-of-all-the-occurrences-of-a-given-string-in-swift-2a2015907a0e
Def could be overkill though, you can also modify the methods in those article to instead to take in an int (occuranceNumber) and use a counter like above to return only the range of the nth occurrence, and then do the same thing with attributed strings.

How to detect UilLabel contain tail "..." IOS/Xamarin

I'm using
titleLabel.Lines = 2;
titleLabel.LineBreakMode = UILineBreakMode.TailTruncation;
Now long text is broken by a ... in the end.
Now I would like to know if the titleLabel is tail truncated , contains "..." ? Any easy suggestions for this ?? as I cannot see the ... characters in the actual titleLabel.Text field
Your question is similar to Change the default '...' at the end of a text if the content of a UILabel doesn't fit
There is no direct option to access ellipsis(the three dot). You need to do it yourself. Code to count the size of your string, clip the string and add a ellipsis with the color you want when the string exceed the view.
Define a NSAttributesString
let atttext = NSAttributedString(string: text!, attributes: [NSForegroundColorAttributeName: UIColor.redColor()])
Calculate the size of the string
let bounds = atttext.boundingRectWithSize(label.bounds.size, options: [], context: nil)
Do something to the string when it exceed the view
if bounds.size.width > 10 {
//Do something here, like assign a new value to `attributedText` of label or change the color
label.attributedText = NSAttributedString(string: "Labelfdjkfdsjkfdsjkf...", attributes: [NSForegroundColorAttributeName: UIColor.blackColor()])
}
For more detail, you can have a look at the last answer of the question I mentioned above.

UITextView attributed Alignment and size

First assume math is correct.
let textViewText = NSMutableAttributedString(string: "34\n+ 10\n+ 32344\n= 23424")
Im using a Textview to display input from the user. To make it easier to read I'm trying to get the text format like this
34
+ 10
+ 32344
= 23424
The other issue I'm having is with wrapping. Is there a way to resize each line to fit on its line?
34
= 23424
4356356
Is your text dynamic or static? If static, then all you need to do is to put the correct amount of spacing between your numbers and plus signs and then right justify the text.
self.textView.attributedText = NSMutableAttributedString(string: "34\n+ 10\n+ 32344\n= 23424")
self.textView.textAlignment = NSTextAlignment.Right
Result:
You can accomplish this by using a right-aligned tab stop in your paragraph style, and separating your operators and values with a tab.
let string = "\t34\n+\t10\n+\t32344\n=\t23424"
let paragraph = NSMutableParagraphStyle()
paragraph.tabStops = [NSTextTab(textAlignment: .Right, location: 200, options: [:])]
let attributedString = NSAttributedString(string: string, attributes:[NSParagraphStyleAttributeName: paragraph])

NSAttributedString with tabs

How do you create a UILabel with this kind of text format? Would you use NSAttributedString?
NSAttributedString can create text columns with tab stops. This is similar to how it is done in a word processor with the same limitations.
let text = "Name\t: Johny\nGender\t: Male\nAge\t: 25\nFavourites\t: Reading, writing"
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.tabStops = [NSTextTab(textAlignment: NSTextAlignment.Left, location: 150, options: [:])]
paragraphStyle.headIndent = 150
label.attributedText = NSAttributedString(string: text, attributes: [NSParagraphStyleAttributeName: paragraphStyle])
tabStops provides point positions for where to continue text after each tab. Here we did one tab at a reasonable point after the first column.
headIndent tells the label that wrapped text needs to be indented by a fixed amount, so it wraps to the next line.
The limitations with this approach are:
The tab stop location is a fixed point value so you need to know what you want. If the value you pick is less than the width of the first column for some lines, those lines will indent to a different location.
Wrapping only really works if your last column is the one that wraps. Since your second column was prefaced by ":" You may want to either just increase your headIndent or also split out the ":" to be \t:\t and set up a second tab stop. If you're not letting text wrap, this is not an issue.
If these limitations are too restrictive, you can restructure your label to be a collection of multiple labels with auto layout constraints.
In Swift 4.2 or above
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.tabStops = [NSTextTab.init(textAlignment: .left, location: 150, options: [:])]
paragraphStyle.headIndent = 150
let attributedTitle = NSAttributedString(string: "Some Title", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14.0), NSAttributedString.Key.paragraphStyle: paragraphStyle])

Resources