Hyphenation in native iOS app - ios

How can I activate automatic hyphenation in iOS?
I have tried to set the hyphenation factor to 1 in the attributed text options of an UILabel, however I don't get any hyphens though.

The iOS 7 way. Use an UITextView instead of an UILabel. The hyphenationFactor (either as a NSParagraphStyle attribute or as a NSLayoutManager property) should work then (thanks to the new TextKit).
The Web way. Use an UIWebView and the -webkit-hyphens CSS properties.
The Core Text or the hard way. Use the CFStringGetHyphenationLocationBeforeIndex() function that you mentioned in a comment. This function only gives you a hint about where to put hyphens in a string for a specific language. Then you have to break your lines of text yourself using the Core Text functions (like CTLineCreateWithAttributedString() and all). See Getting to Know TextKit (the paragraph called Hyphenation explains the logic of the Core Text process, with no code) and Hyphenation with Core Text on the iPad (gives some code sample, but the website seems to be down right now). It's probably going to be more work than you want!

CoreText or TextKit
You need to add "soft hyphenation" to the string. These are "-" which is not visible when rendered, but instead merely queues for CoreText or UITextKit to know how to break up words.
The soft hyphen sign which you should place in the text is:
unichar const kTextDrawingSoftHyphenUniChar = 0x00AD;
NSString * const kTextDrawingSoftHyphenToken = #"­"; // NOTE: UTF-8 soft hyphen!
Example code
NSString *string = #"accessibility tests and frameworks checking";
NSLocale *locale = [NSLocale localeWithLocaleIdentifier:#"en_US"];
NSString *hyphenatedString = [string softHyphenatedStringWithLocale:locale error:nil];
NSLog(#"%#", hyphenatedString);
Outputs ac-ces-si-bil-i-ty tests and frame-works check-ing
NSString+SoftHyphenation.h
typedef enum {
NSStringSoftHyphenationErrorNotAvailableForLocale
} NSStringSoftHyphenationError;
extern NSString * const NSStringSoftHyphenationErrorDomain;
#interface NSString (SoftHyphenation)
- (NSString *)softHyphenatedStringWithLocale:(NSLocale *)locale error:(out NSError **)error;
#end
NSString+SoftHyphenation.m
NSString * const NSStringSoftHyphenationErrorDomain = #"NSStringSoftHyphenationErrorDomain";
#implementation NSString (SoftHyphenation)
- (NSError *)hyphen_createOnlyError
{
NSDictionary *userInfo = #{
NSLocalizedDescriptionKey: #"Hyphenation is not available for given locale",
NSLocalizedFailureReasonErrorKey: #"Hyphenation is not available for given locale",
NSLocalizedRecoverySuggestionErrorKey: #"You could try using a different locale even though it might not be 100% correct"
};
return [NSError errorWithDomain:NSStringSoftHyphenationErrorDomain code:NSStringSoftHyphenationErrorNotAvailableForLocale userInfo:userInfo];
}
- (NSString *)softHyphenatedStringWithLocale:(NSLocale *)locale error:(out NSError **)error
{
CFLocaleRef localeRef = (__bridge CFLocaleRef)(locale);
if(!CFStringIsHyphenationAvailableForLocale(localeRef))
{
if(error != NULL)
{
*error = [self hyphen_createOnlyError];
}
return [self copy];
}
else
{
NSMutableString *string = [self mutableCopy];
unsigned char hyphenationLocations[string.length];
memset(hyphenationLocations, 0, string.length);
CFRange range = CFRangeMake(0, string.length);
for(int i = 0; i < string.length; i++)
{
CFIndex location = CFStringGetHyphenationLocationBeforeIndex((CFStringRef)string,
i,
range,
0,
localeRef,
NULL);
if(location >= 0 && location < string.length)
{
hyphenationLocations[location] = 1;
}
}
for(int i = string.length - 1; i > 0; i--)
{
if(hyphenationLocations[i])
{
[string insertString:#"-" atIndex:i];
}
}
if(error != NULL) { *error = nil;}
return string;
}
}
#end

Swift version:
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.hyphenationFactor = 1
paragraphStyle.alignment = .center
let string = NSAttributedString(string: "wyindywidualizowany indywidualista".uppercased(),
attributes: [NSParagraphStyleAttributeName : paragraphStyle])
myLabel.attributedText = string

Related

Get string between strings with multiple occurrences

I found a lot of examples how to find string between 2 strings, but none of which shows how to handle multiple occurrences of that string. I have for example string like this
"Hi, I am <id>User</id>. I am 20 <id>years old</id>, and live in <id>some country</id>."
The idea behind is that I want to hyperlink each occurrence of that string within UITextField, and remove tags from the string. I also have 2 types of the tag, one should display hyperlink, the other should popup alert view with some text description of the word or phrase clicked.
EDIT:
Found a perfectly good working solution to extend this logic with changing content of the text with attributed string between tags provided in the text. Link here.
#Aleksandar Try this.. it works for me..
NSString *serverOutput = #"Hi, I am <id>User</id>. I am 20 <id>years old</id>, and live in <id>some country</id>.";
if([serverOutput containsString:#"</id>"])
{
NSArray *arrSeparate = [serverOutput componentsSeparatedByString:#"</id>"];
NSString *output = #"";
for(int i=0; i<arrSeparate.count; i++)
{
if([[arrSeparate objectAtIndex:i] containsString:#"<id>"])
{
NSArray *arrTest = [[arrSeparate objectAtIndex:i] componentsSeparatedByString:#"<id>"];
if(output.length < 1)
output = [arrTest objectAtIndex:1];
else
output = [NSString stringWithFormat:#"%#\n%#",output,[arrTest objectAtIndex:1]];
}
}
serverOutput = output;
}
NSLog(#"%#", serverOutput);
Please look into this, and i hope this gets you all the range where the keyword exists
NSString *serverOutput = #"Hi, I am <id>User</id>. I am 20 <id>years old</id>, and live in <id>some country</id>";
NSUInteger count = 0, length = [serverOutput length];
NSRange startRange = NSMakeRange(0, length);
NSRange endRange = NSMakeRange(0, length);
while(startRange.location != NSNotFound)
{
startRange = [serverOutput rangeOfString: #"<id>" options:0 range:startRange];
if(startRange.location != NSNotFound)
{
endRange = [serverOutput rangeOfString: #"</id>" options:0 range:endRange];
startRange = NSMakeRange(startRange.location + startRange.length, length - (startRange.location + startRange.length));
endRange = NSMakeRange(endRange.location + endRange.length, length - (endRange.location + endRange.length));
count++;
}
}
startRange will be the range from where the tag starts and endRange is where starts
You can change the range, location, create attributed string and add hyperlink as the range of string is available to you.

Add attributes to all emoji in an NSAttributedString?

The font I'm using in my iOS app has an unfortunate quality: its characters are unusually small, and as a result, if a user types in a string which includes emoji (or possibly other characters not included in the font? Haven't tested that), when iOS draws those glyphs in the AppleColorEmoji font they come out huge relative to the other glyphs.
This is of course complicated by the fact that emoji are "two-part" glyphs so I can't just do a simple for-each-character-in-the-string loop.
What I need is a method along the lines of
-(NSAttributedString *)attributedStringByAddingAttributes:(NSArray *)attrs toString:(NSString*)myString forCharactersNotInFont:(UIFont *)font
... Or failing that, at least
-(NSAttributedString *)attributedStringByAddingAttributes:(NSArray *)attrs toStringForEmoji:(NSString*)myString
...or something.
Not sure of the best way to do this.
Code I ended up with, using code adapted from here:
- (BOOL)isEmoji:(NSString *)str {
const unichar high = [str characterAtIndex: 0];
// Surrogate pair (U+1D000-1F77F)
if (0xd800 <= high && high <= 0xdbff) {
const unichar low = [str characterAtIndex: 1];
const int codepoint = ((high - 0xd800) * 0x400) + (low - 0xdc00) + 0x10000;
return (0x1d000 <= codepoint && codepoint <= 0x1f77f);
// Not surrogate pair (U+2100-27BF)
} else {
return (0x2100 <= high && high <= 0x27bf);
}
}
// The following takes a string s and returns an attributed string where all the "special characters" contain the provided attributes
- (NSAttributedString *)attributedStringForString:(NSString *)s withAttributesForEmoji:(NSDictionary *)attrs {
NSMutableAttributedString *as = [[NSMutableAttributedString alloc] initWithString:#""];
NSRange fullRange = NSMakeRange(0, [s length]);
[s enumerateSubstringsInRange:fullRange
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString *substring, NSRange substringRange,
NSRange enclosingRange, BOOL *stop)
{
if ([self isEmoji:substring]) {
[as appendAttributedString:[[NSAttributedString alloc] initWithString:substring
attributes:attrs]];
} else {
[as appendAttributedString:[[NSAttributedString alloc] initWithString:substring]];
}
}];
return as;
}
So far, seems to work quite nicely!

Find substring range of NSString with unicode characters

If I have a string like this.
NSString *string = #"😀1😀3😀5😀7😀"
To get a substring like #"3😀5" you have to account for the fact the smiley face character take two bytes.
NSString *substring = [string substringWithRange:NSMakeRange(5, 4)];
Is there a way to get the same substring by using the actual character index so NSMakeRange(3, 3) in this case?
Thanks to #Joe's link I was able to create a solution that works.
This still seems like a lot of work for just trying to create a substring at unicode character ranges for an NSString. Please post if you have a simpler solution.
#implementation NSString (UTF)
- (NSString *)substringWithRangeOfComposedCharacterSequences:(NSRange)range
{
NSUInteger codeUnit = 0;
NSRange result;
NSUInteger start = range.location;
NSUInteger i = 0;
while(i <= start)
{
result = [self rangeOfComposedCharacterSequenceAtIndex:codeUnit];
codeUnit += result.length;
i++;
}
NSRange substringRange;
substringRange.location = result.location;
NSUInteger end = range.location + range.length;
while(i <= end)
{
result = [self rangeOfComposedCharacterSequenceAtIndex:codeUnit];
codeUnit += result.length;
i++;
}
substringRange.length = result.location - substringRange.location;
return [self substringWithRange:substringRange];
}
#end
Example:
NSString *string = #"😀1😀3😀5😀7😀";
NSString *result = [string substringWithRangeOfComposedCharacterSequences:NSMakeRange(3, 3)];
NSLog(#"%#", result); // 3😀5
Make a swift extension of NSString and use new swift String struct. Has a beautifull String.Index that uses glyphs for counting characters and range selecting. Very usefull is cases like yours with emojis envolved

Check IF one String contains the same characters as another string

I am trying to write a function which will allow me to determine whether one NSString* contains the characters of another NSString*. As an example, refer to the below scenario:
NSString *s1 = #"going";
NSString *s2 = #"ievngcogdl";
So essentially when the comparison between these 2 strings occurs, it should return true as the first string s1 has the same characters of the second string s2. Could I use an NSCountedSet? I know that this class has a method containsObject:(id) although I don't think that will solve my problem. Is there any other ways in completing this function and provide me the required results?
I think this method could be rather slow, but I would still favour it over [NSString rangeOfCharacterFromSet:], which requires creating an NSCharacterSet object per comparison:
- (BOOL)string:(NSString *)string containsAllCharactersInString:(NSString *)charString {
NSUInteger stringLen = [string length];
NSUInteger charStringLen = [charString length];
for (NSUInteger i = 0; i < charStringLen; i++) {
unichar c = [charString characterAtIndex:i];
BOOL found = NO;
for (NSUInteger j = 0; j < stringLen && !found; j++)
found = [string characterAtIndex:j] == c;
if (!found)
return NO;
}
return YES;
}
This will work -
-(BOOL) string:(NSString *)string1 containsInputString:(NSString *)string2 {
// Build a set of characters in the string
NSCountedSet *string1Set = [[NSCountedSet alloc]init];
[string1 enumerateSubstringsInRange:NSMakeRange(0, string1.length)
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
[string1Set addObject:substring];
}];
// Now iterated over string 2, removing characters from the counted set as we go
for (int i=0;i<string2.length;i++) {
NSRange range = [string2 rangeOfComposedCharacterSequenceAtIndex:i];
NSString *substring = [string2 substringWithRange:range];
if ([string1Set countForObject:substring]> 0) {
[string1Set removeObject:substring];
}
else {
return NO;
}
}
return YES;
}
Regular Expressions are the best way to check this type of conditions and check this link once
Below I am adding the code for your solution, please check once
NSString *s1 = #"going"
NSString *s2 = #"ievngcogdl";
if ([self string:s1 containsSameCharacterofString:s2]) {
NSLog(#"YES");
}
- (BOOL)string:(NSString *)str containsSameCharacterofString:(NSString *)charString
{
if (charString.length >= str.length) {
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:[NSString stringWithFormat:#"^[%#]+$", charString] options:NSRegularExpressionCaseInsensitive error:&error];
NSRange textRange = NSMakeRange(0, str.length);
NSRange matchRange = [regex rangeOfFirstMatchInString:str options:NSMatchingReportProgress range:textRange];
return (matchRange.location != NSNotFound);
}
else {
return NO;
}
}
BOOL containsString = [#"Hello" containsString:#"llo"];
if (containsString) {
// Do Stuff
}

NSString to treat "regular english alphabets" and characters like emoji or japanese uniformly

There is a textView in which I can enter Characters. characters can be a,b,c,d etc or a smiley face added using emoji keyboard.
-(void)textFieldDidEndEditing:(UITextField *)textField{
NSLog(#"len:%lu",textField.length);
NSLog(#"char:%c",[textField.text characterAtIndex:0]);
}
Currently , The above function gives following outputs
if textField.text = #"qq"
len:2
char:q
if textField.text = #"😄q"
len:3
char:=
What I need is
if textField.text = #"qq"
len:2
char:q
if textField.text = #"😄q"
len:2
char:😄
Any clue how to do this ?
Since Apple screwed up emoji (actually Unicode planes above 0) this becomes difficult. It seems it is necessary to enumerate through the composed character to get the actual length.
Note: The NSString method length does not return the number of characters but the number of code units (not characters) in unichars. See NSString and Unicode - Strings - objc.io issue #9.
Example code:
NSString *text = #"qqq😄rrr";
int maxCharacters = 4;
__block NSInteger unicharCount = 0;
__block NSInteger charCount = 0;
[text enumerateSubstringsInRange:NSMakeRange(0, text.length)
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
unicharCount += substringRange.length;
if (++charCount >= maxCharacters)
*stop = YES;
}];
NSString *textStart = [text substringToIndex: unicharCount];
NSLog(#"textStart: '%#'", textStart);
textStart: 'qqq😄'
An alternative approach is to use utf32 encoding:
int byteCount = maxCharacters*4; // 4 utf32 characters
char buffer[byteCount];
NSUInteger usedBufferCount;
[text getBytes:buffer maxLength:byteCount usedLength:&usedBufferCount encoding:NSUTF32StringEncoding options:0 range:NSMakeRange(0, text.length) remainingRange:NULL];
NSString * textStart = [[NSString alloc] initWithBytes:buffer length:usedBufferCount encoding:NSUTF32LittleEndianStringEncoding];
There is some rational for this in Session 128 - Advance Text Processing from 2011 WWDC.
This is what i did to cut a string with emoji characters
+(NSUInteger)unicodeLength:(NSString*)string{
return [string lengthOfBytesUsingEncoding:NSUTF32StringEncoding]/4;
}
+(NSString*)unicodeString:(NSString*)string toLenght:(NSUInteger)len{
if (len >= string.length){
return string;
}
NSInteger charposition = 0;
for (int i = 0; i < len; i++){
NSInteger remainingChars = string.length-charposition;
if (remainingChars >= 2){
NSString* s = [string substringWithRange:NSMakeRange(charposition,2)];
if ([self unicodeLength:s] == 1){
charposition++;
}
}
charposition++;
}
return [string substringToIndex:charposition];
}

Resources