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

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.

Related

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

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.

UIFont monospaced digits + small caps

I'm trying to create a UIFont with the following attributes:
Upper Case Small Caps
Lower Case Small Caps
Monospaced Digits
I'm using the system font (San Francisco), which does supports all theses features.
As far as I know, the only way to do it is to use multiple UIFontDescriptor.
Here is the code I'm using:
extension UIFont {
var withSmallCaps: UIFont {
let upperCaseFeature = [
UIFontDescriptor.FeatureKey.featureIdentifier : kUpperCaseType,
UIFontDescriptor.FeatureKey.typeIdentifier : kUpperCaseSmallCapsSelector
]
let lowerCaseFeature = [
UIFontDescriptor.FeatureKey.featureIdentifier : kLowerCaseType,
UIFontDescriptor.FeatureKey.typeIdentifier : kLowerCaseSmallCapsSelector
]
let features = [upperCaseFeature, lowerCaseFeature]
let smallCapsDescriptor = self.fontDescriptor.addingAttributes([UIFontDescriptor.AttributeName.featureSettings : features])
return UIFont(descriptor: smallCapsDescriptor, size: pointSize)
}
var withMonospacedDigits: UIFont {
let monospacedDigitsFeature = [
UIFontDescriptor.FeatureKey.featureIdentifier : kNumberSpacingType,
UIFontDescriptor.FeatureKey.typeIdentifier : kMonospacedNumbersSelector
]
let monospacedDigitsDescriptor = self.fontDescriptor.addingAttributes([UIFontDescriptor.AttributeName.featureSettings : [monospacedDigitsFeature]])
return UIFont(descriptor: monospacedDigitsDescriptor, size: pointSize)
}
}
I should be able to obtain a font with all the characteristics mentioned earlier with this line of code:
let font = UIFont.systemFont(ofSize: 16, weight: .regular).withSmallCaps.withMonospacedDigits
// OR
let font = UIFont.monospacedDigitSystemFont(ofSize: 16, weight: .regular).withSmallCaps
But for some reasons, it does not work. I can't get the font to have monospaced digits while having small caps at the same time.
What am I doing wrong?
I figured out why it does not work thanks to the reference document linked by #Carpsen90.
It seems like the Number Spacing feature is exclusive.
As stated in the document:
Features are classified as "exclusive" and "nonexclusive." This indicates whether several different selectors within a given feature type may be selected at once. Thus, it is possible to have both common and rare ligatures turned on, whereas it is impossible to display a given fraction as simultaneously vertical and diagonal.
So having both monospaced digits + small caps features at the same time is impossible.
EDIT:
I misread the document. Selectors of that feature are exclusive. But not the whole feature. So it should be possible.
Have a look at the reference document for more details.
I would suggest using attributed strings with small caps for all glyphs but numbers, and another font for monospaced digits. here is some sample code:
let monoSpacedDigits = UIFont.systemFont(ofSize: 13, weight: .medium).withMonospacedDigits
let smallCaps = UIFont.systemFont(ofSize: 16, weight: .regular).withSmallCaps
let attributedString = NSMutableAttributedString(string: """
H3ll0 7here
1111111111
2222222222
3333333333
4444444444
5555555555
6666666666
7777777777
8888888888
9999999999
0000000000
""", attributes: [NSAttributedStringKey.font : smallCaps])
do {
let regex = try NSRegularExpression(pattern: "[0-9]")
let range = NSRange(0..<attributedString.string.utf16.count)
let matches = regex.matches(in: attributedString.string, range: range)
for match in matches.reversed() {
attributedString.addAttribute(NSAttributedStringKey.font, value: monoSpacedDigits, range: match.range)
}
} catch {
// Do error processing here...
print(error)
}
myLabel.attributedText = attributedString
I've used a size of 13 and a medium weight to make the monospaced digits look as similar as possible to the small caps.

Swift 3 : How do you wrap content in a tableviewcell with multiple labels?

I've been trying to figure this out for a while. I have set constraints for each label with background color set. I set each label's line break to word wrap, but that still doesn't work. What I'm looking for is a label wrap like word wrap whether or not that exists. Thanks.
Here is a slightly different approach. You can customize the appearance of each tagged word with an attributed string. This has some limitations but depending on your requirements it could be a good fit for you. The below code is an example pointing you in the correct direction, however you still might need to write additional code for correctly wrapping the spaces or recognizing touch events.
let tags = ["Outdoors", "Working", "Learning"].map { " \($0) " }
let text = tags.joined(separator: " ")
let ranges = tags.compactMap { text.range(of: $0) }
let attributedText = NSMutableAttributedString(string: text)
for range in ranges {
attributedText.addAttributes([.backgroundColor: UIColor.green], range: NSRange(range, in: text))
}
textView.attributedText = attributedText

Scroll down to, **and a little bit past**, a desired spot in a UITextView

What I have:
Working code that scrolls to and highlights the next instance of a search term in a UITextView that contains a moderately lengthy document.
What I want:
Instead of scrolling to the desired term so that it is at the very bottom of the TextView, I'd like to scroll slightly PAST the term so that it is better visible in the view.
Where I'm stuck:
I'm weak on how Swift generally & Swift 4 in particular work with Range, and NSRange, so while I suspect that the answer will include capturing some amount of the document beyond the search term itself (say, the lesser of the next 200 characters or the next 5 line breaks), I'm not clear how to accomplish that. Or if there is a better approach to scroll the view a bit more.
Code #1 -- does the highlighting and finds "this" instance of the search term:
func attributedTextHighlighting(instanceNbr: Int, searchTerm: String, inRawHaystackText: String) -> NSMutableAttributedString {
let desiredFont = UIFont(name: "Menlo-Regular", size: self.currentDisplayFontSize)
let fontAttribute : [NSAttributedStringKey : Any] = [NSAttributedStringKey.font: desiredFont as Any]
let attributed = NSMutableAttributedString(string: inRawHaystackText, attributes: fontAttribute)
if !searchTerm.isEmpty {
do {
let foo = NSRegularExpression.escapedPattern(for: searchTerm)
let regex = try NSRegularExpression(pattern: foo, options: .caseInsensitive)
var countingMatches = 0
for match in regex.matches(in: inRawHaystackText, range: NSRange(location: 0, length:
inRawHaystackText.utf16.count)) as [NSTextCheckingResult] {
if countingMatches == instanceNbr {
// highlight this term green
attributed.addAttribute(NSAttributedStringKey.backgroundColor, value: UIColor.green, range: match.range)
theGreenMatchingRange = match.range
} else {
// highlight this term yellow
attributed.addAttribute(NSAttributedStringKey.backgroundColor, value: UIColor.yellow, range: match.range)
}
// and either way, increment the countingMatches
countingMatches += 1
}
} catch {
fatalError("Bad RegEx! \(error)")
}
} else {
print("ofSearchTerm.isEmpty = true")
}
return attributed
}
This code finds the search term in the source text & generates attributed text that highlights the term green (for the current "find") or yellow (for all others). This code works OK.
Code #2 -- scrolls to the next instance of the search term
func scrollToNextMatchButtonTapped() {
print("scrollToNextMatchButtonTapped")
currentlyHighlightedInstance += 1
if currentlyHighlightedInstance == numberOfInstances {
currentlyHighlightedInstance = 0 // since numberOfInstances is 1-based but currentlyHighlightedInstance is 0-based
}
reloadAttribTextWithHighlighting() // calls the above code, so the green highlight moves to the next found instance
textDisplay.scrollRangeToVisible(theGreenMatchingRange) // would really like to scroll to here plus a few lines (not past the end of the document, obviously)
The last line there works (theGreenMatchingRange is the global variable that's set in Code #1 above to be the range of "this" instance of the search term), but it scrolls so that the green-highlighted term is at the very bottom of the window.
What do I need to add or do differently so that the green term isn't at the very bottom?

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.

Resources