Prevent UILabel from character wrapping symbols - ios

I have a multi-line label that has the following text:
Lots of text here · $$$$
Since the text at the beginning is freeform, sometimes the wrapping ends up looking like this:
Lots of text here · $$$
$
How do I prevent this from happening? I want it to look like this:
Lots of text here ·
$$$$
I've tried every lineBreakMode to little avail. Word wrap doesn't work because it doesn't treat $$$$ as a word.

It seems that you might benefit from subclassing UILabel, which would treat a string differently for the NSLineBreakByWordWrapping line break mode, which treats your phonetic words like words. You will effectively be expanding the definition by which your custom linebreakmode considers a word.
You would have to roll your own line-breaking algorithm. The approach to determining the location of your line-breaks would be similar to the following:
Loop through the string, to get each character, until one of two conditions is met: a) you have reached the width of the view, or b) you have reached a space, and the next word (delimited by a space) doesn't fit on the same line.
If you have reached condition a, you have two options--you could either adopt a policy that never splits words into multiple lines, or your could only apply the non-split rule to your phonetic words. Either way, you will need to insert a line break at the beginning of the phonetic word, when there is no more room on a given line.
You may want to use two separate strings, to keep the source string separate from the display string that contains your formatting.
Let me know if that helps!

This might be very late but atleast it could help someone.
The way I have done it is as follows:
UIFont *fontUsed = [UIFont systemFontOfSize:17];
NSDictionary *dictFont = [NSDictionary dictionaryWithObject:fontUsed forKey:NSFontAttributeName];
NSString *strTextToShow = #"text that has to be displayed but without $$$$";
CGRect rectForSimpleText = [strTextToShow boundingRectWithSize:CGSizeMake(154, 258) options:NSStringDrawingUsesLineFragmentOrigin attributes:dictFont context:nil];
NSString *strTextAdded = [NSString stringWithFormat:#"%# $$$$", strTextToShow];
CGFloat oldHeight = rectForSimpleText.size.height;
CGRect rectForAppendedText = [strTextAdded boundingRectWithSize:CGSizeMake(154, 258) options:NSStringDrawingUsesLineFragmentOrigin attributes:dictFont context:nil];
CGFloat newHeight = rectForAppendedText.size.height;
if (oldHeight < newHeight) {
strTextAdded = [strTextAdded stringByReplacingOccurrencesOfString:#"$$$$" withString:#"\n$$$$"];
}
[lblLongText setText:strTextAdded];
lblLongText here is the IBOutlet of UILabel and CGSizeMake(154, 258) is the size of UILabel I have used. Let me know if there is any other way you have found.

Try inserting a line break in your input text.
Lots of text here ·\n $$$
It should print the $$$ in the next line.

Related

Inconsistent behavior of kerning in NSAttributedString

I need to use Kern attribute of NSAttributedString. As I can see in the documentation, default value of that attribute is 0.0. But I faced with strange behaviour for phrase Hello, world (for phrase "Hello" all fine):
NSDictionary<NSString*, id>* attributes = #{NSFontAttributeName: [UIFont systemFontOfSize:12]};
NSString* text = #"Hello, World";
NSAttributedString* string = [[NSAttributedString alloc] initWithString:text attributes:attributes];
CGSize size1 = [string size];
NSMutableDictionary<NSString*, id>* attributesWithKernel = [attributes mutableCopy];
attributesWithKernel[NSKernAttributeName] = #(0.0);
NSAttributedString* stringWithKern = [[NSAttributedString alloc] initWithString:text attributes:attributesWithKernel];
CGSize size2 = [stringWithKern size];
XCTAssertTrue(CGSizeEqualToSize(size1, size2)); //here test falls
//size1 = size1 = (width = 68.8125, height = 14.3203125)
//size2 = (width = 69.515625, height = 14.3203125)
To make size1 and size2 equal, kerning should be equal -7.105427357601002e-15 , I know that this is very close to 0.0, but it is strange, because this changes the width almost a pixel.
NSAttributedString has same behaviour in Objective-C and in Swift, example for swift:
let text = "Hello, World"
let attributes : [NSAttributedString.Key : Any] = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: UIFont.systemFontSize)]
let str = NSAttributedString(string: text, attributes: attributes)
let size = str.size()
var attributesWithKern = attributes
attributesWithKern[NSAttributedString.Key.kern] = NSNumber(value: 0.0)
let strWithKern = NSAttributedString(string: text, attributes: attributesWithKern)
let sizeWithKern = strWithKern.size()
XCTAssertTrue(size == sizeWithKern)
How I can fix this behaviour?
P.S.
Now I just remove NSKernAttributeKey from an attribute string if the key equal to 0.0, but I do not think that it is a good solution.
I believe the docs here are wrong, and it is worth opening a radar about that. When no value is set, it is interpreted as "normal kerning." When 0 is set, it is interpreted as "disable kerning," which is why the width is a little wider (kerning typically is slightly negative, bringing kern-pair character, like "W" and "o" in this font, a little closer). I don't think there's any way to explicitly request "default kerning" without removing the attribute.
For your purposes, I believe you're doing the right thing by removing the value when it's zero, because you want default kerning, not to disable kerning.
The reason your tiny negative value is working is because it's not zero, so it's not disabling kerning, but it's so small that the behavior is very, very close to the default, and you're running into the precision of Double in the intermediate calculations (or possibly the precision of Float, depending on how it's implemented internally). You should find that your test passes for any value smaller (closer to zero) than this one, not just that value. In my tests, positive 7e-15 also works, for example.
I did some tests based on your code and I confirm this behaviour which looks like a bug. The point is for some letters strings are equal, for some not. For instance strings with "Hello Mo" or "Hello Oo" are equal, but for instance "Hello WoWoWo" differ a lot. So we see here some kerning being added for "W" without any reason. It can also depend on chosen font, I did not test it though. I see here the only solution that you've used - removing NSKernAttributeKey if it equals to 0.

Replace iOS app emoji with twitter open source twemoji

I want to replace all standard iOS emoji from a UILable or UITextView with twitters open source twemoji.
I can't find any library or documentation to do this in iOS. Does anyone have a solution that does not involve me implementing this from scratch?
The solution needs to be efficient and work offline.
The question got me intrigued, and after a bit of searching on how it would be possible to replace all standard iOS emoji with a custom set, I noticed that even Twitter's own iOS app doesn't use Twemoji:
In the end, I came to the same conclusion as you:
I can't find any library or documentation to do this in iOS.
So, I created a framework in Swift for this exact purpose.
It does all the work for you, but if you want to implement your own solution, I'll describe below how to replace all standard emoji with Twemoji.
1. Document all characters that can be represented as emoji
There are 1126 base characters that have emoji representations, and over a thousand additional representations formed by sequences. Although most base characters are confined to six Unicode blocks, all but one of these blocks are mixed with non-emoji characters and/or unassigned code points. The remaining base characters outside these blocks are scattered across various other blocks.
My implementation simply declares the UTF-32 code points for these characters, as the value property of UnicodeScalar is exactly this.
2. Check whether a character is an emoji
In Swift, a String contains a collection of Character objects, each of which represent a single extended grapheme cluster. An extended grapheme cluster is a sequence of Unicode scalars that together represent one1 human-readable character, which is helpful since you can loop through the Characters of a string and handling them based on the UnicodeScalars they contain (rather than looping through the UTF-16 values of the string).
To identify whether a Character is an emoji, only the first UnicodeScalar is significant, so comparing this value to your table of emoji characters is enough. However, I'd also recommend checking if the Character contains a Variation Selector, and if it does, make sure that it's VS16 – otherwise the character shouldn't be presented as emoji.
Extracting the UnicodeScalars from a Character requires a tiny hack:
let c: Character = "A"
let scalars = String(c).unicodeScalars
3. Convert the code points into the correct format
Twemoji images are named according to their corresponding code points2, which makes sense. So, the next step is to convert the Character into a string equivalent to the image name:
let codePoint = String("🙃").unicodeScalars.first!.value // 128579
let imageName = String(codePoint, radix: 16) // "1f643"
Great, but this won't work for flags or keycaps, so we'll have to modify our code to take those into account:
let scalars = String("🇧🇪").unicodeScalars
let filtered = scalars.filter{ $0.value != 0xfe0f } // Remove VS16 from variants, including keycaps.
let mapped = filtered.map{ String($0.value, radix: 16) }
let imageName = mapped.joined(separator: "-") // "1f1e7-1f1ea"
4. Replace the emoji in the string
In order to replace the emoji in a given String, we'll need to use NSMutableAttributedString for storing the original string, and replace the emoji with NSTextAttachment objects containing the corresponding Twemoji image.
let originalString = "🙃"
let attributedString = NSMutableAttributedString(string: originalString)
for character in originalString.characters {
// Check if character is emoji, see section 2.
...
// Get the image name from the character, see section 3.
let imageName = ...
// Safely unwrapping to make sure the image exists.
if let image = UIImage(named: imageName) {
let attachment = NSTextAttachment()
attachment.image = image
// Create an attributed string from the attachment.
let twemoji = NSAttributedString(attachment: attachment)
// Get the range of the character in attributedString.
let range = attributedString.mutableString.range(of: String(character))
// Replace the emoji with the corresponding Twemoji.
attributedString.replaceCharacters(in: range, with: twemoji)
}
}
To display the resulting attributed string, just set it as the attributedText property of a UITextView/UILabel.
Note that the above method doesn't take into account zero-width joiners or modifier sequences, but I feel like this answer is already too long as it stands.
1. There is a quirk with the Character type that interprets a sequence of joined regional indicator symbols as one object, despite containing a theoretically unlimited amount of Unicode scalars. Try "🇩🇰🇫🇮🇮🇸🇳🇴🇸🇪".characters.count in a playground.
2. The naming pattern varies slightly when it comes to zero-width joiners and variation selectors, so it's easier to strip these out of the image names – see here.
Easiest thing to do:
1) Load the twemoji images into your project.
2) Create an NSDictionary that correlates the emoji codes supported by iOS with the paths to the respective twemoji images:
NSArray *iOSEmojis = #[#"iOSEmoji1",#"iOSEmoji2];
NSDictionary *twemojiPaths = [NSDictionary dictionaryWithObjects:#[#"Project/twemoji1.png",#"Project/twemoji2.png"] andKeys:#[#"iOSEmoji1","iOSEmoji2"]];
3) Code your app to search for emoji strings and display the twemojis where the regular emojis would go:
for (NSString *emoji in iOSEmojis)
{
NSString *twemojiPath = [twemojiPaths valueForKey:emoji];
// Find the position of the emoji string in the text
// and put an image view there.
NSRange range = [label.text rangeOfString:emoji];
NSString *prefix = [label.text substringToIndex:range.location];
CGSize prefixSize = [prefix sizeWithAttributes: #{NSFontAttributeName: [UIFont fontWithName:#"HelveticaNeue" size:14]}];
CGSize emojiSize = [label.text sizeWithAttributes: #{NSFontAttributeName: [UIFont fontWithName:#"HelveticaNeue" size:14]}];
CGRect imageViewFrame = CGRectMake(prefixSize.width,label.frame.size.height,emojiSize.width,label.frame.size.height);
imageViewFrame = [self.view convertRect:imageViewFrame fromView:label];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:imageViewFrame];
imageView.image = [UIImage imageWithContentsOfFile:twemojiPath];
}

How to properly draw underlines in NSLayoutManager

I have currently subclassed NSLayoutManager along with other classes in the UITextView architecture in order to implement my own Word Processor like UITextView. I am currently facing a problem when trying to layout underlines under ranges of glyphs (to implement something like hyperlink). In order to layout the underlines I have overrode the following method in NSLayoutManager.
- (void)drawUnderlineForGlyphRange:(NSRange)glyphRange underlineType (NSUnderlineStyle)underlineVal baselineOffset:(CGFloat)baselineOffset lineFragmentRect:(CGRect)lineRect lineFragmentGlyphRange:(NSRange)lineGlyphRange containerOrigin:(CGPoint)containerOrigin
In the above method I compute the underline distance in points from origin of the underline to the end of the underline via the glyph range. View the following method:
- (void)getStartLocation:(CGFloat * _Nonnull )startLocation andEndLocation:(CGFloat * _Nonnull)endLocation ofGlyphsInRange:(NSRange)glyphRange {
NSRange characterRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
(*startLocation) = [self locationForGlyphAtIndex:glyphRange.location].x;
CGGlyph glyphs[self.numberOfGlyphs];
NSUInteger numGlyphs = [self getGlyphsInRange:glyphRange glyphs:&glyphs[0] properties:NULL characterIndexes:NULL bidiLevels:NULL];
CTFontRef ctfont = [self getCTFontForGlyphsInRange:glyphRange];
double advances = CTFontGetAdvancesForGlyphs(ctfont, kCTFontOrientationDefault, &glyphs[0], NULL, numGlyphs);
(*endLocation) = (*startLocation) + (CGFloat)advances;}
As you can see I get the startLocation, and then I get the endLocation by summing the advances for each glyph in the glyphRange. This computation seems to work reasonably well but it seems to result in a overdrawn or underdrawn line for some sequences of characters. It has recently come to my attention that CTFontGetAdvancesForGlyphs does NOT account for font kerning. Can anyone help me with this code? How can I incorporate font kerning effectively into my computation? I have tried querying the NSKernAttribute in my NSTextStorage at the given location but I keep getting back nil values. I have tried both getting the attributeAtIndex and tried enumerating for the attribute over a range.
I am also aware NSAttributedString has underline options but I want to draw the underline myself for various reasons such as supporting custom defined styles, colors, and more.

Which characters does NSLineBreakByWordWrapping break on?

Short version
From the docs:
NSLineBreakByWordWrapping
Wrapping occurs at word boundaries, unless the word itself doesn’t fit on a single line.
What is the set of all word boundary characters?
Longer version
I have a set of UILabels, which contain text, sometimes including URLs. I need to know the exact location (frame, not range) of the URLs so that I can make them tappable. My math mostly works, but I had to build in a test for certain characters:
// This code only reached if the URL is longer than the available width
NSString *theText = // A string containing an HTTP/HTTPS URL
NSCharacterSet *breakChars = [NSCharacterSet characterSetWithCharactersInString:#"?-"];
NSString *charsInRemaininsSpace = // NSString with remaining text on this line
NSUInteger breakIndex = NSNotFound;
if (charsInRemaininsSpace)
breakIndex = [charsInRemaininsSpace rangeOfCharacterFromSet:breakChars
options:NSBackwardsSearch].location;
if (breakIndex != NSNotFound && breakIndex != theText.length-1) {
// There is a breakable char in the middle, so draw a URL through that, then break
// ...
} else {
// There is no breakable char (or it's at the end), so start this word on a new line
// ...
}
The characters in my NSCharacterSet are just ? and -, which I discovered NSLineBreakByWordWrapping breaks on. It does not break on some other characters I see in URLs like % and =. Is there a complete list of characters I should be breaking on?
I recommend using "not alphanumeric" as your test.
[[NSCharacterSet alphanumerCharacterSet] invertedSet]
That said, if you're doing a lot of this, you may want to consider using CTFramesetter to do your layout instead of UILabel. Then you can use CTRunGetImageBounds to calculate actual rects. You wouldn't have to calculate your word breaks, since CTFrame will already have done this for you (and it would be guaranteed to be the same algorithm).

iOS: Algorithm to center a word in a sentence inside of a UILabel

I need to center a particular word in a sentence by truncating the beginning and endings of a long sentence inside of a UILabel, For example
NSString mySentence = #"This is my very long sentence to give you an example of what I am trying to do.. And its still going..";
NSString myWord = #"example";
<Algorithm goes here>
Should display:
"...sentence to give you an example of what I am trying to..."
If the word is closer to one end or the other, just do your best to center and display an appropriate amount of the text, example:
NSString myWord = #"my";
"This is my very long sentence to give you an example of what..."
Any ideas?
Thanks.
I believe you can do a scan for your search to be placed. Let's say you have the index in a variable named centerWordIndex. You could then split the string based on whitechars, and add words to the beginning and the end of your word until you are out of words at each side, or until the size of you string matches the size of the label.
what do you think :) ?
The data you need to start is:
the width of your label (call it labelWidth)
the width of the word you want to center (call it wordWidth)
Then the size on each side you have to work with is (labelWidth - wordWidth) / 2. Don't worry about using this value, it's just the target.
You can use the ability to calculate the size of a NSString using
CGSize newSize = [myString sizeWithFont: myFont];
CGFloat newWidth = newSize.width;
The goal of the algorithm should be to continue to add words on each side of the centered word and recalculating the total width each step. If you've gone past labelWidth, you can't add that word or anymore from that direction. So, in pseudocode, one approach is:
calculate labelWidth and wordWidth
set currentWidth to wordWidth
set currentLeftPosition to position of first letter of word
set currentRightPosition to position of last letter of word
set currentString to word
set currentImbalance to 0
algorithm start:
scan for position of 2 spaces to left of currentLeftPosition or start of string
set leftTrialPosition to position found
set leftTrialString as between leftTrialPosition and currentRightPosition inclusive
calculate trialLeftWidth of leftTrialString
scan for position of 2 spaces to right of currentRightPosition or end of string
set rightTrialPosition to position found
set rightTrialString as between currentLeftPosition and rightTrialPositon inclusive
calculate trialRightWidth of rightTrialString
if (trialLeftWidth - currentImbalance <= trialRightWidth
&& trialLeftWidth <= labelWidth
&& trialLeftWidth != currentWidth)
set currentImbalance -= calculate width of string from leftTrialPosition to currentLeftPosition
set currentLeftPosition = leftTrialPosition
set currentWidth = trialLeftWidth
set currentString = leftTrialString
else if (same checks for right)
same steps using right data
else if (both left and right are larger than label or both sides no longer grow bigger)
algorithm is done - return here
recurse to algorithm start
Using this basic strategy, you track the left imbalance (negative) or right imbalance (positive) throughout the algorithm and preferentially add the left or right word accordingly until you have the biggest string of complete words that can fit on the label or you have used the full string. The key iOS specific part here is the NSString method that calculates the width.

Resources