Even after #user3441734 solved most of my problems ๐, there are a few emoji that I can't seem to render properly when converting from a [String:String] to String.
Here's some Playground-ready code to illustrate the problem:
var u = ""
u = "1f468-1f468-1f467-1f467" // ๐จโ๐จโ๐งโ๐ง
//u = "1f918-1f3ff" // ๐ค๐ฟ
//u = "1f468-2764-1f48b-1f468" // ๐จโโคโ๐โ๐จ (broken)
//u = "1f3c7-1f3fb" // ๐โ๐ป (broken)
let unicodeArray = u.characters.split("-")
.map(String.init)
.map {String(UnicodeScalar(Int($0,radix: 16) ?? 0))}
if let last = unicodeArray.last {
let separator: String
switch (unicodeArray.first, last) {
// Failed attempt to get tone applied to jockey
case let (horse_racing, _) where horse_racing == "\u{1f3c7}":
separator = "\u{200d}"
case let (_, tone) where "\u{1f3fb}"..."\u{1f3ff}" ~= tone:
separator = ""
case let (_, regionalIndicatorSymbol) where "\u{1f1e6}"..."\u{1f1ff}" ~= regionalIndicatorSymbol:
separator = ""
default:
separator = "\u{200d}"
}
print(unicodeArray.joinWithSeparator(separator))
}
Uncomment each assignment to u in turn to see the problem in action. The 3rd and 4th values should render like so:
and
Thoughtsโฆ
It turns out that a long-press on the race horse fails to show skin tones on iOS, so let's assume that's just an oversight, perhaps related to the near-impossibility of judging the jockey's skin tone at standard emoji sizes. I still can't figure out the problem with u = "1f468-2764-1f48b-1f468"
Apologies if this question comes out at all unclear. Chrome and Safari have different behaviors w.r.t these combo-emoji, so only the linked images are guaranteed to appear to you the way I see them on my end. ๐ข
These emoji are all either skin-tone renderings or tokens of same-sex affection. Is there some sort of bizarre latent racism & homophobia lurking in the system?! ๐ฑ (Cue the conspiracy theories.)
Note that my attempt to use the zero-width joiner, u{200d} didn't help.
So, bug in Apple & Chrome's handling of certain emoji, or is there yet another idiosyncrasy of the standard that I've missed?
There's no conspiracy, the bugs are in your code.
The first character can be produced with:
U+1F468 U+200D U+2764 U+FE0F U+200D U+1F48B U+200D U+1F468
Note the ZERO WIDTH JOINER (U+200D) between each character, and the VARIATION SELECTOR-16 selector (U+FE0F) on the HEAVY BLACK HEART (U+2764) to ensure the emoji presentation style is used.
Refer to this table for a complete list of implemented multi-person groupings.
U+1F3C7 HORSE RACING is not an emoji modifier base, and so it does not support skin tone modifiers.
Related
For example i have this:
TAKE, O take those lips away
That so sweetly were forsworn,
And those eyes, the break of day,
Lights that do mislead the morn:
But my kisses bring again,
Bring againโ
Seals of love, but sealโd in vain,
Sealโd in vain!
โ William Shakespeare
And i want to replace some words to TextField(in Compose) or to EditText(in XML).
Example:
TAKE, O take those lips textfield
That so were forsworn,
And those eyes, the break of day,
Lights that do mislead the morn:
But my textfield bring again,
Bring againโ
textfield of love, textfield sealโd in vain,
Sealโd in vain!
โ William Shakespeare
Can you advise me the way to realise it? Maybe specific libraries?
First of all, I highlighted the words to be replaced by * so that they can be easily found using a regular expression.
val string = """
TAKE, O take those lips *away*
That so sweetly were forsworn,
And those eyes, the break of day,
Lights that do mislead the morn:
But my *kisses* bring again,
Bring againโ
*Seals* of love, *but* sealโd in vain,
Sealโd in vain!
โ William Shakespeare
""".trimIndent()
val matches = remember(string) { Regex("\\*\\w+\\*").findAll(string) }
Using the onTextLayout argument in Compose Text, you can get a lot of information about the text to be rendered, including the positions of each character. And the indexes of the characters you need to replace are already defined by a regular expression.
All you have to do is place the text fields at the appropriate positions.
I use BasicTextField because it doesn't have the extra padding that TextField has, so the size is easy to match with Text. I set its background to white so that the original text doesn't shine through. If you have an unusual background, a gradient for example, you can also make the text transparent with annotated text as shown in the documentation, then the BasicTextField can be left transparent.
The SubcomposeLayout is a great tool for creating such layouts without waiting a next recomposition to use onTextLayout result.
val textLayoutState = remember { mutableStateOf<TextLayoutResult?>(null) }
val textFieldTexts = remember(matches.count()) { List(matches.count()) { "" }.toMutableStateList() }
val style = MaterialTheme.typography.body1
SubcomposeLayout { constraints ->
val text = subcompose("text") {
Text(
text = string,
style = style,
onTextLayout = {
textLayoutState.value = it
},
)
}[0].measure(constraints)
val textLayout = textLayoutState.value ?: run {
// shouldn't happen as textLayoutState is updated during sub-composition
return#SubcomposeLayout layout(0, 0) {}
}
val wordsBounds = matches.map {
// I expect all selected words to be on a single line
// otherwise path bounds will take both lines
textLayout
.getPathForRange(it.range.first, it.range.last + 1)
.getBounds()
}
val textFields = wordsBounds.mapIndexed { i, wordBounds ->
subcompose("textField$i") {
BasicTextField(
value = textFieldTexts[i],
onValueChange = {
textFieldTexts[i] = it
},
onTextLayout = {
println("${it.size}")
},
textStyle = style,
modifier = Modifier
.border(1.dp, Color.LightGray)
.background(Color.White)
)
}[0].measure(Constraints(
maxWidth = floor(wordBounds.width).toInt(),
)) to wordBounds.topLeft
}
layout(text.width, text.height) {
text.place(0, 0)
textFields.forEach {
val (placeable, position) = it
placeable.place(floor(position.x).toInt(), floor(position.y).toInt())
}
}
}
Result:
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];
}
I am quite new to programing in swift and I am working on a music app for iOS that adjusts the font size of the text in a UILabel in proportion to the string's length. In my code, I am trying to count the number of characters in the string statement and have it plugged into a formula, but for some reason Xcode gives me the error: Cannot call value of non function type double I tried setting the value to a CGFloat but it still gives me the same error on the "let b = 41.2 - .8(a) line. Thank you so much and sorry if this seems like a basic question.
let title = "Let It Bleed"
AlbumName.text = title
let a = title.characters.count
if ( a <= 19){
let b = 41.2 - .8(a)
let fontsize = CGFloat(b)
AlbumName.font = AlbumName.font.fontWithSize(fontsize)
}
A screenshot of the code with the error
I assume you expect "0.8 times a" with .8(a).
Three things:
You need leading 0 to represent fractional values in Swift.
You need explicit operator * for multiplication.
You need to convert numeric types to match for mathematical operations.
All these included, your line of interest becomes like this:
let b = 41.2 - 0.8 * CGFloat(a)
I have a UITextView with items looking like this:
artist1 - song name1, artist2 - song name 2,
artist3 - song name3, etc.
I only want to disallow line breaks except between artists (after comma, before next artist), so this would be invalid:
artist 1 - song name1, artist 2 - // <- invalid break
song name 2.
I have tried two approaches for accomplishing this: Firstly, I put things in a webview, making this trivial to solve. Unfortunately, that is not possible for a couple of other reasons.
Secondly, I added _ where there are white spaces and hid them, like this:
artist_1_-_song_name1, artist_2_-_song_name_2, etc // <- setting colour of _ to invisible.
This almost worked, but now it breaks at the hyphen, like this:
artist_1_-
song_name1
I'm firstly looking for a good way to solve this. If that doesn't work, I would settle for a Unicode Char that looks like a dash but doesn't break (the U+2015 horizontal bar is too long)
Edit: Just found out about about Unicode Character 'NON-BREAKING HYPHEN' (U+2011), so while the problem is technically solved, I'm still looking for a less hackish way to do it though.
What you want is a non breaking space, or Unicode Character u+00a0.
NSString *nonbreakingspace = #"\u00A0";
NSString *nonbreakinghyphen = #"\u2011";
An replace your spaces in the artist1 - song name1 with the nonbreakingspace string and the hyphen with the non breaking hyphen nonbreakinghyphen string.
NSString *text = ...;
text = [text stringByReplacingOccurrencesOfString:#" " withString:nonbreakingspace];
text = [text stringByReplacingOccurrencesOfString:#"-" withString:nonbreakinghyphen];
myTextView.text = text;
swift 4 version!
var nonbreakinghyphen = "\u{2011}"
var text = "i-21"
myTextView.text = text.replacingOccurrences(of: "-", with: nonbreakinghyphen)}
I have read this,but it can only work well in English for it just use white-space and something like NewlineCharacterSet as separator.
I want to add a left arrow and a right arrow in the accessory input view to move the cursor in UITextView by words.
And I am wondering how to support that feature for some Asian languages like Chinese
PS:I will added an example that CFStringTokenizer failed to work with when there are both English Characters and Chinese characters
test string:
Happy Christmas! Text view test ไบๅญๅจๅฎนๅจๆต่ฏๅผๅฟ yeap
the expected boundaries:
Happy/ Christmas!/ Text/ view/ test/ ไบ/ๅญๅจ/ๅฎนๅจ/ๆต่ฏ/ๅผๅฟ/ yeap/
the boundaries show in reality:
Happy/ Christmas!/ Text/ view/ test/ ไบๅญๅจๅฎนๅจๆต่ฏๅผๅฟ/ yeap/
I don't speak Chinese, but according to the documentation,
CFStringTokenizer is able to find word boundaries in many languages,
including Asian languages.
The following code shows how to advance from one word ("world" at position 6)
to the next word ("This" at position 13):
// Test string.
NSString *string = #"Hello world. This is great.";
// Create tokenizer
CFStringTokenizerRef tokenizer = CFStringTokenizerCreate(NULL,
(__bridge CFStringRef)(string),
CFRangeMake(0, [string length]),
kCFStringTokenizerUnitWordBoundary,
CFLocaleCopyCurrent());
// Start with a position that is inside the word "world".
CFIndex position = 6;
// Goto current token ("world")
CFStringTokenizerTokenType tokenType;
tokenType = CFStringTokenizerGoToTokenAtIndex(tokenizer, position);
if (tokenType != kCFStringTokenizerTokenNone) {
// Advance to next "normal" token:
tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer);
while (tokenType != kCFStringTokenizerTokenNone && tokenType != kCFStringTokenizerTokenNormal) {
tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer);
}
if (tokenType != kCFStringTokenizerTokenNone) {
// Get the location of next token in the string:
CFRange range = CFStringTokenizerGetCurrentTokenRange(tokenizer);
position = range.location;
NSLog(#"%ld", position);
// Output: 13 = position of the word "This"
}
}
There is no CFStringTokenizerAdvanceToPreviousToken() function, so to move to
the previous word you have to start at the beginning of the string and advance forward.
Finnally I use UITextInputTokenizer to realized the function