UIFont: How to use Stylistic Alternate character? - ios

In my App I want to use stylistic alternate font for 'a' and not system font.
Attaching screenshot which explains the different rendering of the font.
How can I enable this behaviour for UILabel and UITextView so that it renders the correct One Storey 'a'?
I did find a YouTube video link which explains exactly this but he is using a custom font and it is hardcoded. I want to use system font only but with this alternate character.
I might be able to hardcode UILabel with the custom character, I am not sure because I want to use System font. I don't want to use custom Font. What about UITextView which is editable? How can we make it use alternate a as and when the user types?

This is a font feature called "Alternative Stylistic Sets" that you can configure with CoreText. Remember that not all fonts have this option, but the system fonts do. You need to figure out which alternative set you want, however.
First, create the font you're interested in:
import CoreText
import UIKit
let baseFont = UIFont.systemFont(ofSize: 72)
Then print out its features:
print(CTFontCopyFeatures(baseFont)!)
Find the section on Alternative Stylistic Sets, and specifically the set you want, "One storey a:"
{
CTFeatureTypeIdentifier = 35;
CTFeatureTypeName = "Alternative Stylistic Sets";
CTFeatureTypeSelectors = (
{
CTFeatureSelectorIdentifier = 2;
CTFeatureSelectorName = "Straight-sided six and nine";
},
{
CTFeatureSelectorIdentifier = 4;
CTFeatureSelectorName = "Open four";
},
{
CTFeatureSelectorIdentifier = 6;
CTFeatureSelectorName = "Vertically centered colon";
},
{
CTFeatureSelectorIdentifier = 10;
CTFeatureSelectorName = "Vertically compact forms";
},
{
CTFeatureSelectorIdentifier = 12;
CTFeatureSelectorName = "High legibility";
},
{
CTFeatureSelectorIdentifier = 14;
CTFeatureSelectorName = "One storey a";
},
...
The important number is the selector (CTFeatureSelectorIdentifier), 14. With that you can create a new font descriptor and new font:
let descriptor = CTFontDescriptorCreateCopyWithFeature(
baseFont.fontDescriptor,
kStylisticAlternativesType as CFNumber,
14 as CFNumber)
Or you can do this directly in UIKit if it's more convenient:
let settings: [UIFontDescriptor.FeatureKey: Int] = [
.featureIdentifier: kStylisticAlternativesType,
.typeIdentifier: 14
]
let descriptor = baseFont.fontDescriptor.addingAttributes([.featureSettings: [settings]])
(Note the somewhat surprising fact that .featureIdentifier is "CTFeatureTypeIdentifier" and .typeIdentifier is "CTFeatureSelectorIdentifier".)
And then you can create a new font (a zero size means to leave the size the same):
let font = UIFont(descriptor: descriptor, size: 0)
You can use that anywhere that accepts a UIFont.

Here is an extension to simplify choosing an alternative stylistic set for a font, assuming you know its name:
Example usage:
guard let updatedFont = font.withAlternativeStylisticSet(withName: "Alternate y") else {
fatalError("Alternative stylistic set is undefined")
}
UIFont extension:
import UIKit
import CoreText
extension UIFont {
/// Returns the font, applying an alternative stylistic style set.
func withAlternativeStylisticSet(withName name: String) -> UIFont? {
guard let identifier = alternativeStylisticSetIdentifier(withName: name) else {
return nil
}
let settings: [UIFontDescriptor.FeatureKey: Int] = [
.featureIdentifier: kStylisticAlternativesType,
.typeIdentifier: identifier
]
let fontDescriptor = self.fontDescriptor.addingAttributes([.featureSettings: [settings]])
return UIFont(descriptor: fontDescriptor, size: 0)
}
/// Returns the identifier for an alternative stylistic set
private func alternativeStylisticSetIdentifier(withName selectorName: String) -> Int? {
guard let ctFeatures = CTFontCopyFeatures(self) else {
return nil
}
let features = ctFeatures as [AnyObject] as NSArray
for feature in features {
if let featureDict = feature as? [String: Any] {
if let typeName = featureDict[kCTFontFeatureTypeNameKey as String] as? String {
if typeName == "Alternative Stylistic Sets" {
if let featureTypeSelectors = featureDict[kCTFontFeatureTypeSelectorsKey as String] as? NSArray {
for featureTypeSelector in featureTypeSelectors {
if let featureTypeSelectorDict = featureTypeSelector as? [String: Any] {
if let name = featureTypeSelectorDict[kCTFontFeatureSelectorNameKey as String] as? String, let identifier = featureTypeSelectorDict[kCTFontFeatureSelectorIdentifierKey as String] as? Int {
if name == selectorName {
return identifier
}
}
}
}
}
}
}
}
}
return nil
}
}

Related

How to get the Lowest and Highest Value from Dictionary in Swift

I am facing a problem in getting the values to make the desired combination. I am using a filter screen in my app. I asked this question to get the first and last element from the question How to put the of first and last element of Array in Swift and it is working but the problem is in my FiterVC first I selected the option $400 - $600 and then I selected the $200 - $400. After selecting I am getting these values in my currentFilter variable.
private let menus = [
["title": "Price", "isMultiSelection": true, "values": [
["title": "$00.00 - $200.00"],
["title": "$200.00 - $400.00"],
["title": "$400.00 - $600.00"],
["title": "$600.00 - $800.00"],
["title": "$800.00 - $1000.00"],
]],
["title": "Product Rating", "isMultiSelection": true, "values": [
["title": "5"],
["title": "4"],
["title": "3"],
["title": "2"],
["title": "1"]
]],
["title": "Arriving", "isMultiSelection": true, "values": [
["title": "New Arrivials"],
["title": "Coming Soon"]
]]
]
private var currentFilters = [String:Any]()
Selecting values in didSelect method:-
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView === self.menuTableView {
self.currentSelectedMenu = indexPath.row
self.menuTableView.reloadData()
self.valueTableView.reloadData()
}
else {
if let title = self.menus[self.currentSelectedMenu]["title"] as? String, let values = self.menus[self.currentSelectedMenu]["values"] as? [[String:Any]], let obj = values[indexPath.row]["title"] as? String {
if let old = self.selectedFilters[title] as? [String], let isAllowedMulti = self.menus[self.currentSelectedMenu]["isMultiSelection"] as? Bool, !old.isEmpty, !isAllowedMulti {
var temp = old
if old.contains(obj), let index = old.index(of: obj) {
temp.remove(at: index)
}
else {
temp.append(obj)
}
self.selectedFilters[title] = temp
}
else {
self.selectedFilters[title] = [obj]
}
self.valueTableView.reloadData()
}
}
}
And on Apply button click:-
#IBAction func applyButtonAction(_ sender: UIButton) {
self.delegate?.didSelectedFilters(self, with: self.selectedFilters)
printD(self.selectedFilters)
self.dismiss(animated: true, completion: nil)
}
When I am printing the selectedFilters I am getting these values:-
currentFilters ["Price": ["$400.00 - $600.00", "$200.00 - $400.00"]]
And by using this method I am getting the first and last value from dictionary like this:-
if let obj = currentFilters["Price"] as? [String] {
self.priceRange = obj
printD(self.priceRange)
let first = priceRange.first!.split(separator: "-").first!
let last = priceRange.last!.split(separator: "-").last!
let str = "\(first)-\(last)"
let str2 = str.replacingOccurrences(of: "$", with: "", options: NSString.CompareOptions.literal, range: nil)
newPrice = str2
printD(newPrice)
}
The result is :-
400.00 - 400.00
but what I actually want is 200 - 600. How can I do this. Please help?
However there are many solutions available to this problem but simplest and most relevant solution is highlighted here:
let min = 1000, max = 0
if let obj = currentFilters["Price"] as? [String] {
self.priceRange = obj
printD(self.priceRange)
for str in obj{
let first = str.split(separator: "-").first!.replacingOccurrences(of: "$", with: "", options:
NSString.CompareOptions.literal, range: nil)
let last = str.split(separator: "-").last!.replacingOccurrences(of: "$", with: "", options:
NSString.CompareOptions.literal, range: nil)
if Int(first) < min{
min = first
}
if Int(last) > max{
max = last
}
}
let str = "\(min)-\(max)"
newPrice = str
printD(newPrice)
}
You can use an enum with functions to load your prices ranges instead of using directly Strings.
enum PriceRange: String {
case
low = "200 - 400",
high = "400 - 600"
static let priceRanges = [low, high]
func getStringValue() -> String {
return self.rawValue
}
func getMinValueForRange(stringRange: String) -> Int? {
switch stringRange {
case "200 - 400":
return 200;
case "400 - 600":
return 400;
default:
return nil
}
}
func getMaxValueForRange(stringRange: String) -> Int? {
switch stringRange {
case "200 - 400":
return 400;
case "400 - 600":
return 600;
default:
return nil
}
}
}
Then you can use/add functions to get the result that you are looking for.
Here's the small solution of your problem. It all breaks down to that: you get all the values from your filters, and then iterating over getting all the values from it. That way you avoid comparing the pairs and instead comparing the exact values.
let currentFilters = ["Price": ["$400.00 - $600.00", "$200.00 - $400.00"]]
let ranges = currentFilters["Price"]!
var values: [Double] = []
ranges.forEach { range in // iterate over each range
let rangeValues = range.split(separator: "-")
for value in rangeValues { // iterate over each value in range
values.append(Double( // cast string value to Double
String(value)
.replacingOccurrences(of: " ", with: "")
.replacingOccurrences(of: "$", with: "")
)!)
}
}
let min = values.min() // prints 200
let max = values.max() // prints 600
We can do by steps -
1 - Get the price ranges from the dictionary
2 - Iterate over those price ranges
3 - Split the range by "-" and check if the prices count is 2 else this is an invalid price range
4 - Get the numeric components from the two prices and then compare from the previously saved min and max values and update them accordingly
Try this -
if let priceRanges = currentFilters["Price"] as? [String] { // Extract the price ranges from dictionary
var minValue: Double? // Holds the min value
var maxValue: Double? // Holds the max value
for priceRange in priceRanges { Iterate over the price ranges
let prices = priceRange.split(separator: "-") // Separate price range by "-"
guard prices.count == 2 else { // Checks if there are 2 prices else price range is invalid
print("invalid price range")
continue // skip this price range when invalid
}
let firstPrice = String(prices[0]).numericString // Extract numerics from the first price
let secondPrice = String(prices[1]).numericString // Same for the second price
if let value = Double(firstPrice) { // Check if the price is valid amount by casting it to double
if let mValue = minValue { // Check if we have earlier saved a minValue from a price range
minValue = min(mValue, value) // Assign minimum of current price and earlier save min price
} else {
minValue = value // Else just save this price to minValue
}
}
if let value = Double(secondPrice) { // Check if the price is valid amount by casting it to double
if let mValue = maxValue { // Check if we have earlier saved a maxValue from a price range
maxValue = max(mValue, value) // Assign maximum of current price and earlier save max price
} else {
maxValue = value // Else just save this price to maxValue
}
}
}
if let minV = minValue, let maxV = maxValue { // Check if we have a min and max value from the price ranges
print("\(minV) - \(maxV)")
} else {
print("unable to find desired price range") // Else print this error message
}
}
extension String {
/// Returns a string with all non-numeric characters removed
public var numericString: String {
let characterSet = CharacterSet(charactersIn: "01234567890.").inverted
return components(separatedBy: characterSet)
.joined()
}
}

How to improve NSTextStorage addAttribute performance

I have a long text (probably usual book like >200 pages) in NSTextStorage.
I'm distributing this text for textContainers this way:
let textStorageLength = defaultTextStorage?.length ?? 0
while layoutManager!.textContainer(forGlyphAt: textStorageLength - 1,
effectiveRange: nil) == nil {
let textContainer = NSTextContainer(size: textContainerSize)
layoutManager!.addTextContainer(textContainer)
pagesCount += 1
}
Then I use this containers for textViews on pageViewController.
I have some words marking like:
func selectTappableWordsFromList(_ list: [PositionWithWord]) {
self.defaultTextStorage?.beginEditing()
list.forEach {
if !self.shouldInterruptCurrentProcesses {
self.markWordInDefaultTextStorage(positionWithWord: $0)
} else {
self.shouldInterruptCurrentProcesses = false
self.defaultTextStorage?.endEditing()
return
}
}
self.defaultTextStorage?.endEditing()
}
func markWordInDefaultTextStorage(positionWithWord: PositionWithWord) {
let range = NSMakeRange(positionWithWord.position!.start,
positionWithWord.position!.length)
defaultTextStorage?.addAttributes(defaultAttributes, range: range)
}
let defaultAttributes: [NSAttributedStringKey: Any] = [
.underlineStyle: NSUnderlineStyle.styleSingle.rawValue,
.underlineColor: UIColor.BookReader.underline
]
The problem is: marking words in whole text storage is working very slow. The more pages in the book, the slower it works.
Also screenshot of cpu usage from profiler.
Any way to improve logic?

Swift code to produce a number of possible anagrams from a selected word

I've attempted to research ways to take a given word and calculate the number of possible anagrams a user can make from that word eg an 8 letter word such as snowbanks has 5 eight letter possibilities, 25 seven letter possibilities, etc (those are made up numbers). My initial plan would be to iterate over a dictionary list and check each of the words to see if it is an anagram of the word in question as I've seen suggested in other places.
Rearrange Letters from Array and check if arrangement is in array
seemed very promising, except that it is in objective C and when I tried to convert it to Swift using Swiftify I couldn't get it to work as shown below:
func findAnagrams() -> Set<AnyHashable>? {
let nineCharacters = [unichar](repeating: 0, count: 8)
let anagramKey = self.anagramKey()
// make sure this word is not too long/short.
if anagramKey == nil {
return nil
}
(anagramKey as NSString?)?.getCharacters(nineCharacters, range: NSRange)
let middleCharPos = Int((anagramKey as NSString?)?.range(of: (self as NSString).substring(with: NSRange)).location ?? 0)
var anagrams = Set<AnyHashable>()
// 0x1ff means first 9 bits set: one for each character
for i in 0...0x1ff {
// skip permutations that do not contain the middle letter
if (i & (1 << middleCharPos)) == 0 {
continue
}
var length: Int = 0
var permutation = [unichar](repeating: 0, count: 9)
for bit in 0...9 {
if true {
permutation[length] = nineCharacters[bit]
length += 1
}
}
if length < 4 {
continue
}
let permutationString = String(permutation)
let matchingAnagrams = String.anagramMap()[permutationString] as? [Any]
for word: String? in matchingAnagrams {
anagrams.insert(word)
}
}
return anagrams
}
class func anagramMap() -> [AnyHashable: Any]? {
var anagramMap: [AnyHashable: Any]
if anagramMap != nil {
return anagramMap
}
// this file is present on Mac OS and other unix variants
let allWords = try? String(contentsOfFile: "/usr/share/dict/words", encoding: .utf8)
var map = [AnyHashable: Any]()
autoreleasepool {
allWords?.enumerateLines(invoking: {(_ word: String?, _ stop: UnsafeMutablePointer<ObjCBool>?) -> Void in
let key = word?.anagramKey()
if key == nil {
return
}
var keyWords = map[key] as? [AnyHashable]
if keyWords == nil {
keyWords = [AnyHashable]()
map[key] = keyWords
}
if let aWord = word {
keyWords?.append(aWord)
}
})
}
anagramMap = map
return anagramMap
}
func anagramKey() -> String? {
let lowercaseWord = word.lowercased()
// make sure to take the length *after* lowercase. it might change!
let length: Int = lowercaseWord.count
// in this case we're only interested in anagrams 4 - 9 characters long
if length < 3 || length > 9 {
return nil
}
let sortedWord = [unichar](repeating: 0, count: length)
(lowercaseWord as NSString).getCharacters(sortedWord, range: NSRange)
qsort_b(sortedWord, length, MemoryLayout<unichar>.size, {(_ aPtr: UnsafeRawPointer?, _ bPtr: UnsafeRawPointer?) -> Int in
let a = Int(unichar(aPtr))
let b = Int(unichar(bPtr))
return b - a
})
return String(describing: sortedWord)
}
func isReal(word: String) -> Bool {
let checker = UITextChecker()
let range = NSMakeRange(0, word.utf16.count)
let misspelledRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
return misspelledRange.location == NSNotFound
}
}
I've also tried the following in an attempt to just produce a list of words that I could iterate over to check for anagrams (I have working code that checks guesses vs the main word to check for anagrams) but I wasn't able to get them to work, possibly because they require a file to be copied to the app, since I was under the impression that the phone has a dictionary preloaded that I could use for words (although I may be mistaken):
var allTheWords = try? String(contentsOfFile: "/usr/share/dict/words", encoding: .utf8)
for line: String? in allTheWords?.components(separatedBy: "\n") ?? [String?]() {
print("\(line ?? "")")
print("Double Fail \(allTheWords)")
}
and
if let wordsFilePath = Bundle.main.path(forResource: "dict", ofType: nil) {
do {
let wordsString = try String(contentsOfFile: wordsFilePath)
let wordLines = wordsString.components(separatedBy: NSCharacterSet.newlines)
let randomLine = wordLines[Int(arc4random_uniform(UInt32(wordLines.count)))]
print(randomLine)
} catch { // contentsOfFile throws an error
print("Error: \(error)")
}
}
}
I looked at UIReferenceLibraryViewController as well in an attempt to use it to produce a list of words instead of defining a selected word, but the following isn't a valid option.
let words = UIReferenceLibraryViewController.enumerated
Any assistance would be greatly appreciated!

How to know data coming from JSON is a Float or an Integer in Swift 3?

I am getting data from Json and displaying it in table view how to check whether the number is float or double or integer in swift 3 if it is float how to get the no.of digits after decimal can anyone help me how to implement this in swift 3 ?
if specialLoop.attributeCode == "special_price" {
let attributeString: NSMutableAttributedString = NSMutableAttributedString(string: "$ \((arr.price))")
attributeString.addAttribute(NSStrikethroughStyleAttributeName, value: 1, range: NSMakeRange(0, attributeString.length))
let specialPrice = specialLoop.value.replacingOccurrences(of: ".0000", with: "0")
print(specialPrice)
cell.productPrice.text = "$ \(specialPrice)"
cell.specialPriceLabel.isHidden = false
cell.specialPriceLabel.attributedText = attributeString
break
}
else {
cell.specialPriceLabel.isHidden = true
let price = arr.price
print(price)
cell.productPrice.text = "$ \( (price))0"
}
You can use (if let)
let data = [String: Any]()
if let value = data["key"] as? Int {
} else if let value = data["key"] as? Float {
} else if let value = data["key"] as? Double {
}
as describe below, you can find a type of any object (whether custom class or built-in class like - String, Int, etc.).
class demo {
let a: String = ""
}
let demoObj = demo()
print(type(of: demoObj))
--> Output: "demo.Type"

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