Swift - Textview Identify Tapped Word Not Working - ios

Long time user, first time poster, so my apologies if I make any errors in presenting my question. I have been working on this for hours and I've decided it is time to ask the experts. I have also searched through every similar question that has been "answered" and work, which leads me to believe they are outdated.
I am attempting to grab the tapped word from a UITextview that would be used later in the code. For example, there is a paragraph of words in the text view:
"The initial return on time investment is much smaller, due to him trading his upfront cost for sweat-equity in the company, but the potential long-term payout is much greater".
I would want to be able to tap on a word, e.g. 'investment', and run it through another function to define it. However simply tapping the word, crashes the program, and I do not receive the word tapped.
I implemented a tap gesture recognizer:
let tap = UITapGestureRecognizer(target: self, action: #selector(tapResponse(_:)))
tap.delegate = self
tvEditor.addGestureRecognizer(tap)
and then wrote the function: 2
func tapResponse(recognizer: UITapGestureRecognizer) {
let location: CGPoint = recognizer.locationInView(tvEditor)
let position: CGPoint = CGPointMake(location.x, location.y)
let tapPosition: UITextPosition = tvEditor.closestPositionToPoint(position)!
let textRange: UITextRange = tvEditor.tokenizer.rangeEnclosingPosition(tapPosition, withGranularity: UITextGranularity.Word, inDirection: 1)!
let tappedWord: String = tvEditor.textInRange(textRange)!
print("tapped word : %#", tappedWord)
}
Ideally, this should take the location from the tapped part of the Textview, take the position by taking the .x & .y, and then looking through the Textview at the point closest to the position, finding the Range enclosing the position with granularity (to return the word), and setting the contents as a String, which I am currently just printing to the console. However, on tapping the word, I receive this crash.3
along with "fatal error: unexpectedly found nil while unwrapping an Optional value" in the console.
Any help would be greatly appreciated. I may just be missing something simple, or it could be much more complicated.

Whenever the tapped received at the blank spaces between the words, tapPosition returned by the TextView can be nil nil.
Swift has new operator called optional ? which tells the compiler that the variable may have nil value. If you do not use ? after the variable name indicates that the variable can never have nil value.
In Swift, using ! operator means you are forcing the compiler to forcefully extract the value from the optional variable. So, in that case, if the value of the variable is nil, it will crash on forcing.
So, what is actually happening is
You are creating the variable let tapPosition: UITextPosition, let textRange: UITextRange and let tappedWord: String are not optional
return type of the method myTextView.closestPositionToPoint(position), tvEditor.textInRange(textRange) are optional variable UITextPosition?, String?
Assigning a value of optional variable to non optional variable requires !
The method is returning nil and you are forcing it to get the value ! lead to CRASH
What you can do
Before forcing any optional variable, just be sure that it has some value using
if variable != nil
{
print(variable!)
}
Correct method would be as
func tapResponse(recognizer: UITapGestureRecognizer) {
let location: CGPoint = recognizer.locationInView(myTextView)
let position: CGPoint = CGPointMake(location.x, location.y)
let tapPosition: UITextPosition? = myTextView.closestPositionToPoint(position)
if tapPosition != nil {
let textRange: UITextRange? = myTextView.tokenizer.rangeEnclosingPosition(tapPosition!, withGranularity: UITextGranularity.Word, inDirection: 1)
if textRange != nil
{
let tappedWord: String? = myTextView.textInRange(textRange!)
print("tapped word : ", tappedWord)
}
}
}

Swift 3.0 Answer - Working as of July 1st, 2016
In my ViewDidLoad() -
I use text from a previous VC, so my variable "theText" is already declared. I included a sample string that has been noted out.
//Create a variable of the text you wish to attribute.
let textToAttribute = theText // or "This is sample text"
// Break your string in to an array, to loop through it.
let textToAttributeArray = textToAttribute.componentsSeparatedByString(" ")
// Define a variable as an NSMutableAttributedString() so you can append to it in your loop.
let attributedText = NSMutableAttributedString()
// Create a For - In loop that goes through each word your wish to attribute.
for word in textToAttributeArray{
// Create a pending attribution variable. Add a space for linking back together so that it doesn't looklikethis.
let attributePending = NSMutableAttributedString(string: word + " ")
// Set an attribute on part of the string, with a length of the word.
let myRange = NSRange(location: 0, length: word.characters.count)
// Create a custom attribute to get the value of the word tapped
let myCustomAttribute = [ "Tapped Word:": word]
// Add the attribute to your pending attribute variable
attributePending.addAttributes(myCustomAttribute, range: myRange)
print(word)
print(attributePending)
//append 'attributePending' to your attributedText variable.
attributedText.appendAttributedString(attributePending) ///////
print(attributedText)
}
textView.attributedText = attributedText // Add your attributed text to textview.
Now we will add a tap gesture recognizer to register taps.
let tap = UITapGestureRecognizer(target: self, action: #selector(HandleTap(_:)))
tap.delegate = self
textView.addGestureRecognizer(tap) // add gesture recognizer to text view.
Now we declare a function under the viewDidLoad()
func HandleTap(sender: UITapGestureRecognizer) {
let myTextView = sender.view as! UITextView //sender is TextView
let layoutManager = myTextView.layoutManager //Set layout manager
// location of tap in myTextView coordinates
var location = sender.locationInView(myTextView)
location.x -= myTextView.textContainerInset.left;
location.y -= myTextView.textContainerInset.top;
// character index at tap location
let characterIndex = layoutManager.characterIndexForPoint(location, inTextContainer: myTextView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
// if index is valid then do something.
if characterIndex < myTextView.textStorage.length {
// print the character index
print("Your character is at index: \(characterIndex)") //optional character index.
// print the character at the index
let myRange = NSRange(location: characterIndex, length: 1)
let substring = (myTextView.attributedText.string as NSString).substringWithRange(myRange)
print("character at index: \(substring)")
// check if the tap location has a certain attribute
let attributeName = "Tapped Word:" //make sure this matches the name in viewDidLoad()
let attributeValue = myTextView.attributedText.attribute(attributeName, atIndex: characterIndex, effectiveRange: nil) as? String
if let value = attributeValue {
print("You tapped on \(attributeName) and the value is: \(value)")
}
}
}

In addition to #AmitSingh answer, this is updated Swift 3.0 version:
func didTapTextView(recognizer: UITapGestureRecognizer) {
let location: CGPoint = recognizer.location(in: textView)
let position: CGPoint = CGPoint(x: location.x, y: location.y)
let tapPosition: UITextPosition? = textView.closestPosition(to: position)
if tapPosition != nil {
let textRange: UITextRange? = textView.tokenizer.rangeEnclosingPosition(tapPosition!, with: UITextGranularity.word, inDirection: 1)
if textRange != nil
{
let tappedWord: String? = textView.text(in: textRange!)
print("tapped word : ", tappedWord!)
}
}
}
The other code is the same as his.
Hope it helps!

Related

Place cursor at the end of UITextView under UITest

This is how I clear UITextFields and UITextViews in UITests.
extension XCUIElement {
func clear() {
tap()
while (value as! String).characters.count > 0 {
XCUIApplication().keys["delete"].tap()
}
}
}
Example of use:
descriptionTextView.type("Something about Room.")
descriptionTextView.clear()
If I run UITests, it always tap at the beginning of UITextView.
How to tap at the end?
You can tap on the lower right corner to place the cursor at the end of the text view.
Additionally you can improve the speed of deletion by preparing a deleteString containing a number of XCUIKeyboardKeyDelete that wipes your entire text field at once.
extension XCUIElement {
func clear() {
guard let stringValue = self.value as? String else {
XCTFail("Tried to clear and enter text into a non string value")
return
}
let lowerRightCorner = self.coordinateWithNormalizedOffset(CGVectorMake(0.9, 0.9))
lowerRightCorner.tap()
let deleteString = [String](count: stringValue.characters.count + 1, repeatedValue: XCUIKeyboardKeyDelete)
self.typeText(deleteString.joinWithSeparator(""))
}
}
This is Tomas Camin's solution adapted for Swift 5.3 (Xcode 12):
extension XCUIElement {
public func clear() {
guard let stringValue = self.value as? String else {
XCTFail("Tried to clear and enter text into a non string value")
return
}
let lowerRightCorner = self.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.9))
lowerRightCorner.tap()
let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count)
self.typeText(deleteString)
}
}

How to convert character index from layoutManager to String scale in swift

How to convert character index from layoutManager to String scale in swift?
this is the code I'm using:
let touchPoint: CGPoint = gesture.locationOfTouch(0, inView: self.definitionLabel)
let index = layoutManager.characterIndexForPoint(touchPoint, inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
please don't tell me to use advanceBy() function on the first index of the string characterset since characters like ò count two in the scale of layoutManager but swift string counts theme once.
The index returned from the NSLayoutManager is "NSString based",
i.e. it is the number of UTF-16 code units from the start of the string
to the character at the given point. (So ò actually counts as
one, but Emojis 😀 count two and flags 🇧🇪 even count four.)
To convert that index to a valid Swift String index, you can use
the same approach as in https://stackoverflow.com/a/30404532/1187415:
let text = ... // the stored text
let i16 = text.utf16.startIndex.advancedBy(index, limit: text.utf16.endIndex)
// i16 is a String.UTF16View.Index
if let strIndex = String.Index(i16, within: text) {
// strIndex is a String.CharacterView.Index
let char = text[strIndex]
print(char)
}
Updated for Swift 5
Martin R's answer gave me the rough outline; here's working Swift 5 version. Mine is for a UITextView with Attributed Text but should work just as well with regular String and for UITextField and UILabel.
func handleTap(_ sender: UIGestureRecognizer) {
guard let textView = sender.view as? UITextView else { return }
guard let plaintext = textView.attributedText?.string else { return }
//guard let plaintext = textView.text else { return }
let location = sender.location(in: textView)
let charIndex = textView.layoutManager.characterIndex(for: location, in: textView.textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
if let strIndex = plaintext.utf16.index(plaintext.utf16.startIndex, offsetBy: charIndex, limitedBy: plaintext.utf16.endIndex) {
let char = plaintext[strIndex]
print("Character tapped was \(char)")
}
}
let textTap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
textView.addGestureRecognizer(textTap)

Get character before cursor in Swift

Yesterday I was working on getting and setting the cursor position in a UITextField. Now I am trying to get the character just before the cursor position. So in the following example, I would want to return an "e".
func characterBeforeCursor() -> String
Notes
I didn't see any other SO questions that were the same of this, but maybe I missed them.
I wrote this question first and when I find an answer I will post both the question and the answer at the same time. Of course, better answers are welcomed.
I said "character" but String is fine.
If the cursor is showing and the position one place before it is valid, then get that text. Thanks to this answer for some hints.
func characterBeforeCursor() -> String? {
// get the cursor position
if let cursorRange = textField.selectedTextRange {
// get the position one character before the cursor start position
if let newPosition = textField.position(from: cursorRange.start, offset: -1) {
let range = textField.textRange(from: newPosition, to: cursorRange.start)
return textField.text(in: range!)
}
}
return nil
}
The result of
if let text = characterBeforeCursor() {
print(text)
}
is "e", as per your example.
You can also use this:
NSInteger endOffset = [textfld offsetFromPosition:textfld.beginningOfDocument toPosition:range1.end];
NSRange offsetRange = NSMakeRange(endOffset-1, 1);
NSString *str1 = [textfld.text substringWithRange:offsetRange];
NSLog(#"str1= %#",str1);
In swift you can use
let range1 : UITextRange = textField.selectedTextRange!
let endoffset : NSInteger = textField.offsetFromPosition(textField.beginningOfDocument, toPosition: range1.end)
let offsetRange : NSRange = NSMakeRange(endoffset-1, 1)
let index: String.Index = (textField.text?.startIndex.advancedBy(offsetRange.location))!
let str1 : String = (textField.text?.substringFromIndex(index))!
let index1 : String.Index = str1.startIndex.advancedBy(1)
let str2: String = str1.substringToIndex(index1)
print(str2)

Segmentation Fault 11 when running Swift app

I have just updated to the newest Xcode 6.3 beta and I am receiving an error that I can't figure out the solution to.
When I run my app, I am getting the error: Command failed due to signal: Segmentation Fault 11. I had a look through the full error message and I have taken the parts out that I think are relevant:
(unresolved_dot_expr type='#lvalue String!' location=/Filename/FifthViewController.swift:423:19 range=[/Filename/FifthViewController.swift:423:9 - line:423:19] field 'text'
(declref_expr type='UITextField' location=/Filename/FifthViewController.swift:423:9 range=[/Filename/FifthViewController.swift:423:9 - line:423:9] decl=AffordIt.(file).FifthViewController.func decl.textField#/Filename/FifthViewController.swift:418:34 specialized=yes))
And
While emitting SIL for 'textFieldDidChangeValue' at /Filename/FifthViewController.swift:418:5
Anyone have any ideas? Obviously I've replaced the full path with 'Filename'. This is the code related to the error:
textField.addTarget(self, action: "textFieldDidChangeValue:", forControlEvents: UIControlEvents.EditingChanged)
func textFieldDidChangeValue(textField: UITextField) {
//Automatic formatting for transaction value text field. Target is added above.
var text = textField.text.stringByReplacingOccurrencesOfString(currencyFormatter.currencySymbol!, withString: "").stringByReplacingOccurrencesOfString(currencyFormatter.groupingSeparator, withString: "").stringByReplacingOccurrencesOfString(currencyFormatter.decimalSeparator!, withString: "").stringByReplacingOccurrencesOfString(" ", withString: "") // There is a special character here. This line is critical for european/other currencies.
println(textField.text)
textField.text = currencyFormatter.stringFromNumber((text as NSString).doubleValue / 100.0)
currencyDouble = (text as NSString).doubleValue / 100.0
valueEnter.alpha = 1
}
Here is the initialisation of currencyFormatter:
let currencyFormatter = NSNumberFormatter()
currencyFormatter.numberStyle = NSNumberFormatterStyle.CurrencyStyle
if let currencyCode = NSLocale.currentLocale().objectForKey(NSLocaleCurrencyCode) as? String {
currencyFormatter.currencyCode = currencyCode
}
It seems, the problem is in this line:
textField.text = currencyFormatter.stringFromNumber((text as NSString).doubleValue / 100.0)
It must be a compiler bug. The workarounds I found are:
// Explicitly cast as `NSNumber`
textField.text = currencyFormatter.stringFromNumber((text as NSString).doubleValue / 100.0 as NSNumber)
// or explicitly construct `NSNumber` from `Double`
textField.text = currencyFormatter.stringFromNumber(NSNumber(double: (text as NSString).doubleValue / 100.0))
// or prepare `Double` outside
let doubleVal = (text as NSString).doubleValue / 100.0
textField.text = currencyFormatter.stringFromNumber(doubleVal)
// or convert `String` to `Double` without casting to `NSString`.
textField.text = currencyFormatter.stringFromNumber( atof(text) / 100.0)
The minimum code that reproduces this problem would be:
let str = "42"
let a:NSNumber = (str as NSString).doubleValue / 100.0
Swift 1.1/Xcode 6.1.1 compiles it successfully, but Swift 1.2/Xcode 6.3 Beta2 crashes.
This might be a long shot, but how is your currencyFormatter declared? Is it marked with an ! and not intialised? I used the following code to test for a faulty declaration and it behaved rather strangely in my Swift iOS Playground:
var myTextField = UITextField()
var currencyFormatter : NSNumberFormatter! // Possible offender! Not initiliased, and forcefully unwrapped!!!
func myFunc(textField: UITextField) {
//Automatic formatting for transaction value text field. Target is added above.
var text : String = textField.text // Error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION
//if currencyFormatter != nil {
text = textField.text
.stringByReplacingOccurrencesOfString(currencyFormatter.currencySymbol!, withString: "")
.stringByReplacingOccurrencesOfString(currencyFormatter.groupingSeparator, withString: "")
.stringByReplacingOccurrencesOfString(currencyFormatter.decimalSeparator!, withString: "")
.stringByReplacingOccurrencesOfString(" ", withString: "") // There is a special character here. This line is critical for european/other currencies.
println("original: \(textField.text), after replacing: \(text)")
textField.text = currencyFormatter.stringFromNumber((text as NSString).doubleValue / 100.0)
println("new: \(textField.text)")
// currencyDouble = (text as NSString).doubleValue / 100.0
// valueEnter.alpha = 1
// } else {
// println("No currencyFormatter")
// }
}
//currencyFormatter = NSNumberFormatter()
// Uncomment line above, and the playground failed on second line in myFunc()
myTextField.text = "$123.000,00"
myFunc(myTextField)
myTextField.text
If this is your case as well, you need to look into the missing initialisation of the currencyFormatter, and also uncomment the if statements from my code excerpt to avoid it crashing everything. If uncommenting, you'll also see if this is the actual error case for you as there is an else clause stating that the currencyFormatter actually is nil.
The reason for me looking into this case, was to see if you had a problem with unwrapping of currencyFormatter.currencySymbol! and currencyFormatter.decimalSeparator!, but testing revealed they could be nil without casing any other issue than text == "".
You might also want to look into the NSNumberFormatter.numberFromString() method, see following:
var currencyFormatter = NSNumberFormatter()
currencyFormatter.numberStyle = NSNumberFormatterStyle.CurrencyStyle
if let numberStr = currencyFormatter.stringFromNumber(NSNumber(double: 123567.45)) {
if let number = currencyFormatter.numberFromString(numberStr) {
println("numberStr = \(numberStr) vs number = \( number )")
}
}
which outputs numberStr = $123,567.45 vs number = 123567.45 after converting back and forth.
I had the same problem here and it was not related to the code itself. all of them disappeared after upgrading to Xcode 6.3 beta 2 released by Apple two days ago. give it a try

Ruby text/ Furigana in ios

I am currently trying to display some text in Japanese on a UITextView. Is it possible to display the furigana above the kanji (like below) in a manner similar to the < rt> tag in html, without using a web view?
A lot of text processing is involved, therefore I cannot simply use a web view. In iOS8, CTRubyAnnotationRef was added but there is no documentation (I would welcome any example), and I am also concerned with the lack of compatibility with iOS7. I thought that it would be possible to display the furigana above with the use of an NSAttributedString, but couldn't as of yet.
Update Swift 5.1
This solution is an update of preview answers and let you write Asian sentences with Phonetic Guide, using a pattern in the strings.
Let's start from handling string.
these 4 extension let you to inject in a string the ruby annotation.
the function createRuby() check the string a pattern, that it is: |word written in kanji《phonetic guide》.
Examples:
|紅玉《ルビー》
|成功《せいこう》するかどうかは、きみの|努力《どりょく》に|係《かか》る。
and so on.
the important thing is to follow the pattern.
extension String {
// 文字列の範囲
private var stringRange: NSRange {
return NSMakeRange(0, self.utf16.count)
}
// 特定の正規表現を検索
private func searchRegex(of pattern: String) -> NSTextCheckingResult? {
do {
let patternToSearch = try NSRegularExpression(pattern: pattern)
return patternToSearch.firstMatch(in: self, range: stringRange)
} catch { return nil }
}
// 特定の正規表現を置換
private func replaceRegex(of pattern: String, with templete: String) -> String {
do {
let patternToReplace = try NSRegularExpression(pattern: pattern)
return patternToReplace.stringByReplacingMatches(in: self, range: stringRange, withTemplate: templete)
} catch { return self }
}
// ルビを生成
func createRuby() -> NSMutableAttributedString {
let textWithRuby = self
// ルビ付文字(「|紅玉《ルビー》」)を特定し文字列を分割
.replaceRegex(of: "(|.+?《.+?》)", with: ",$1,")
.components(separatedBy: ",")
// ルビ付文字のルビを設定
.map { component -> NSAttributedString in
// ベース文字(漢字など)とルビをそれぞれ取得
guard let pair = component.searchRegex(of: "|(.+?)《(.+?)》") else {
return NSAttributedString(string: component)
}
let component = component as NSString
let baseText = component.substring(with: pair.range(at: 1))
let rubyText = component.substring(with: pair.range(at: 2))
// ルビの表示に関する設定
let rubyAttribute: [CFString: Any] = [
kCTRubyAnnotationSizeFactorAttributeName: 0.5,
kCTForegroundColorAttributeName: UIColor.darkGray
]
let rubyAnnotation = CTRubyAnnotationCreateWithAttributes(
.auto, .auto, .before, rubyText as CFString, rubyAttribute as CFDictionary
)
return NSAttributedString(string: baseText, attributes: [kCTRubyAnnotationAttributeName as NSAttributedString.Key: rubyAnnotation])
}
// 分割されていた文字列を結合
.reduce(NSMutableAttributedString()) { $0.append($1); return $0 }
return textWithRuby
}
}
Ruby Label: the big problem
As you maybe know, Apple has introduced in iOS 8 the ruby annotation like attribute for the attributedString, and if you did create the the attributed string with ruby annotation and did:
myLabel.attributedText = attributedTextWithRuby
the label did shows perfectly the string without problem.
From iOS 11, Apple unfortunately has removed this feature and, so, if want to show ruby annotation you have override the method draw, to effectively draw the text. To do this, you have to use Core Text to handle the text hand it's lines.
Let's show the code
import UIKit
public enum TextOrientation { //1
case horizontal
case vertical
}
class RubyLabel: UILabel {
public var orientation:TextOrientation = .horizontal //2
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
// ルビを表示
override func draw(_ rect: CGRect) {
//super.draw(rect) //3
// context allows you to manipulate the drawing context (i'm setup to draw or bail out)
guard let context: CGContext = UIGraphicsGetCurrentContext() else {
return
}
guard let string = self.text else { return }
let attributed = NSMutableAttributedString(attributedString: string.createRuby()) //4
let path = CGMutablePath()
switch orientation { //5
case .horizontal:
context.textMatrix = CGAffineTransform.identity;
context.translateBy(x: 0, y: self.bounds.size.height);
context.scaleBy(x: 1.0, y: -1.0);
path.addRect(self.bounds)
attributed.addAttribute(NSAttributedString.Key.verticalGlyphForm, value: false, range: NSMakeRange(0, attributed.length))
case .vertical:
context.rotate(by: .pi / 2)
context.scaleBy(x: 1.0, y: -1.0)
//context.saveGState()
//self.transform = CGAffineTransform(rotationAngle: .pi/2)
path.addRect(CGRect(x: self.bounds.origin.y, y: self.bounds.origin.x, width: self.bounds.height, height: self.bounds.width))
attributed.addAttribute(NSAttributedString.Key.verticalGlyphForm, value: true, range: NSMakeRange(0, attributed.length))
}
attributed.addAttributes([NSAttributedString.Key.font : self.font], range: NSMakeRange(0, attributed.length))
let frameSetter = CTFramesetterCreateWithAttributedString(attributed)
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0,attributed.length), path, nil)
// Check need for truncate tail
//6
if (CTFrameGetVisibleStringRange(frame).length as Int) < attributed.length {
// Required truncate
let linesNS: NSArray = CTFrameGetLines(frame)
let linesAO: [AnyObject] = linesNS as [AnyObject]
var lines: [CTLine] = linesAO as! [CTLine]
let boundingBoxOfPath = path.boundingBoxOfPath
let lastCTLine = lines.removeLast() //7
let truncateString:CFAttributedString = CFAttributedStringCreate(nil, "\u{2026}" as CFString, CTFrameGetFrameAttributes(frame))
let truncateToken:CTLine = CTLineCreateWithAttributedString(truncateString)
let lineWidth = CTLineGetTypographicBounds(lastCTLine, nil, nil, nil)
let tokenWidth = CTLineGetTypographicBounds(truncateToken, nil, nil, nil)
let widthTruncationBegins = lineWidth - tokenWidth
if let truncatedLine = CTLineCreateTruncatedLine(lastCTLine, widthTruncationBegins, .end, truncateToken) {
lines.append(truncatedLine)
}
var lineOrigins = Array<CGPoint>(repeating: CGPoint.zero, count: lines.count)
CTFrameGetLineOrigins(frame, CFRange(location: 0, length: lines.count), &lineOrigins)
for (index, line) in lines.enumerated() {
context.textPosition = CGPoint(x: lineOrigins[index].x + boundingBoxOfPath.origin.x, y:lineOrigins[index].y + boundingBoxOfPath.origin.y)
CTLineDraw(line, context)
}
}
else {
// Not required truncate
CTFrameDraw(frame, context)
}
}
//8
override var intrinsicContentSize: CGSize {
let baseSize = super.intrinsicContentSize
return CGSize(width: baseSize.width, height: baseSize.height * 1.0)
}
}
Code explanation:
1- Chinese and japanese text can be written in horizontal and vertical way. This enumeration let you switch in easy way between horizontal and vertical orietantation.
2- public variable with switch orientation text.
3- this method must be commented. the reason is that call it you see two overlapping strings:one without attributes, last your attributed string.
4- here call the method of String class extension in which you create the attributed string with ruby annotation.
5- This switch, rotate if need the context in which draw your text in case you want show vertical text. In fact in this switch you add the attribute NSAttributedString.Key.verticalGlyphForm that in case vertical is true, false otherwise.
6- This 'if' is particular important because, the label, cause we had commented the method 'super.draw()' doesn't know how to manage a long string. without this 'if', the label thinks to have only one line to draw. And so, you still to have a string with '...' like tail. In this 'if' the string is broken in more line and drawing correctly.
7- When you don't give to label some settings, the label knows to have more one line but because it can't calculate what is the showable last piece of string, give error in execution time and the app goes in crash. So be careful. But, don't worry! we talk about the right settings to give it later.
8- this is very important to fit the label to text's size.
How to use the RubyLabel
the use of the label is very simple:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var rubyLabel: RubyLabel! //1
override func viewDidLoad() {
super.viewDidLoad()
setUpLabel()
}
private func setUpLabel() {
rubyLabel.text = "|成功《せいこう》するかどうかは、きみの|努力《どりょく》に|係《かか》る。|人々《ひとびと》の|生死《せいし》に|係《かか》る。" //2
//3
rubyLabel.textAlignment = .left
rubyLabel.font = .systemFont(ofSize: 20.0)
rubyLabel.orientation = .horizontal
rubyLabel.lineBreakMode = .byCharWrapping
}
}
Code Explanation:
1- connect label to xib if use storyboard or xib file or create label.
2- as I say, the use is very simple: here assign the string with ruby pattern like any other string
3- these setting are the setting to have to set to make work the label. You can set via code or via storyboard/xib
Be careful
if you use storyboard/xib, if you don't put correctly the constraints, the label give you the error at point n° 7.
Result
Works, but not perfect
As you can see by screenshot, this label work well but still has some problem.
1- with vertical text the label still in horizontal shape;
2- if the string contains \n to split the string in more lines, the label shows only the number of lines that the string would have had if was without the '\n' character.
I'm working for fix these problem, but your help is appreciated.
In the end I created a function that gets the kanji's position and create labels every time, above the kanji. It's quite dirty but it's the only way to insure a compatibility with iOS7.
However I have to mention the remarkable examples that you can find on GitHub for the CTRubyAnnotationRef nowadays like : http://dev.classmethod.jp/references/ios8-ctrubyannotationref/
and
https://github.com/shinjukunian/SimpleFurigana
Good luck to all of you !
Here's a focused answer with some comments. This works for UITextView and UILabel on iOS 16.
let rubyAttributes: [CFString : Any] = [
kCTRubyAnnotationSizeFactorAttributeName : 0.5,
kCTRubyAnnotationScaleToFitAttributeName : 0.5,
]
let annotation = CTRubyAnnotationCreateWithAttributes(
.center, // Alignment relative to base text
.auto, // Overhang for adjacent characters
.before, // `before` = above, `after` = below, `inline` = after the base text (for horizontal text)
"Ruby!" as CFString,
rubyAttributes as CFDictionary
)
let stringAttributes = [kCTRubyAnnotationAttributeName as NSAttributedString.Key : annotation]
NSAttributedString(string: "Base Text!", attributes: stringAttributes)
Note, you may want to UITextView.textContainerInset.top to something larger than the default to avoid having the ruby clipped by the scrollview.

Resources