How to improve NSTextStorage addAttribute performance - ios

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?

Related

Show More/ Show less in the end of UILabel in Swift

I need to implement show more/ show less in UILabel like facebook. My label has mention and url features. Everything worked perfectly when there is no emoji in the text. I have added trailing at the end of the label by calculating textHeight and visible text of the label.
This is my extension file
//
// extension.swift
// SeeMore
//
// Created by Macbook Pro on 14/10/20.
//
import Foundation
import UIKit
//MARK : - text height, weidth
extension Range where Bound == String.Index {
var nsRange:NSRange {
return NSRange(location: self.lowerBound.encodedOffset,
length: self.upperBound.encodedOffset -
self.lowerBound.encodedOffset)
}
}
extension String {
var extractURLs: [URL] {
var urls : [URL] = []
var error: NSError?
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
var text = self
detector!.enumerateMatches(in: text, options: [], range: NSMakeRange(0, text.count), using: { (result: NSTextCheckingResult!, flags: NSRegularExpression.MatchingFlags, stop: UnsafeMutablePointer<ObjCBool>) -> Void in
// println("\(result)")
print("Extracted Result: \(result.url)")
urls.append(result.url!)
})
return urls
}
func textHeight(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
return boundingBox.height
}
func textWidth(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return boundingBox.width
}
}
extension UILabel{
func indexOfAttributedTextCharacterAtPoint(point: CGPoint, font : UIFont) -> Int {
guard let attributedString = self.attributedText else { return -1 }
let mutableAttribString = NSMutableAttributedString(attributedString: attributedString)
// Add font so the correct range is returned for multi-line labels
mutableAttribString.addAttributes([NSAttributedString.Key.font: font], range: NSRange(location: 0, length: attributedString.length))
let textStorage = NSTextStorage(attributedString: mutableAttribString)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: frame.size)
textContainer.lineFragmentPadding = 0
textContainer.maximumNumberOfLines = numberOfLines
textContainer.lineBreakMode = lineBreakMode
layoutManager.addTextContainer(textContainer)
let index = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return index
}
func addTrailingForShowLess(with trailingText: String, moreText: String, moreTextFont: UIFont, moreTextColor: UIColor, complition: #escaping (_ attribute: NSMutableAttributedString) -> Void) {
let readMoreText: String = trailingText + moreText
if let myText = self.text {
let trimmedString: String? = myText + trailingText
let readMoreLength: Int = (readMoreText.count)
guard let safeTrimmedString = trimmedString else { return }
if safeTrimmedString.count <= readMoreLength { return }
print("less this number \(safeTrimmedString.count) should never be less\n")
print("less then this number \(readMoreLength)")
// "safeTrimmedString.count - readMoreLength" should never be less then the readMoreLength because it'll be a negative value and will crash
// let trimmedForReadMore: String = (safeTrimmedString as NSString).replacingCharacters(in: NSRange(location: safeTrimmedString.count, length: readMoreLength), with: "") + trailingText
let answerAttributed = NSMutableAttributedString(string: safeTrimmedString, attributes: [NSAttributedString.Key.font: moreTextFont])
let readMoreAttributed = NSMutableAttributedString(string: moreText, attributes: [NSAttributedString.Key.font: moreTextFont, NSAttributedString.Key.foregroundColor: moreTextColor])
answerAttributed.append(readMoreAttributed)
complition(answerAttributed)
// self.attributedText = answerAttributed
}
}
func addTrailing(with trailingText: String, moreText: String, moreTextFont: UIFont, moreTextColor: UIColor, complition: #escaping (_ attribute: NSMutableAttributedString) -> Void) {
let readMoreText: String = trailingText + moreText
if self.text?.count == 0 { return }
if self.visibleTextLength == 0 { return }
let lengthForVisibleString: Int = self.visibleTextLength
if let myText = self.text {
let mutableString: String = myText
let trimmedString: String? = (mutableString as NSString).replacingCharacters(in: NSRange(location: lengthForVisibleString, length: myText.count - lengthForVisibleString), with: "")
let readMoreLength: Int = (readMoreText.count + 2)
guard let safeTrimmedString = trimmedString else { return }
if safeTrimmedString.count <= readMoreLength { return }
print("this number \(safeTrimmedString.count) should never be less\n")
print("then this number \(readMoreLength)")
// "safeTrimmedString.count - readMoreLength" should never be less then the readMoreLength because it'll be a negative value and will crash
let trimmedForReadMore: String = (safeTrimmedString as NSString).replacingCharacters(in: NSRange(location: safeTrimmedString.count - readMoreLength, length: readMoreLength), with: "") + trailingText
let answerAttributed = NSMutableAttributedString(string: trimmedForReadMore, attributes: [NSAttributedString.Key.font: moreTextFont])
let readMoreAttributed = NSMutableAttributedString(string: moreText, attributes: [NSAttributedString.Key.font: moreTextFont, NSAttributedString.Key.foregroundColor: moreTextColor])
answerAttributed.append(readMoreAttributed)
complition(answerAttributed)
// self.attributedText = answerAttributed
}
}
var visibleTextLength: Int {
let font: UIFont = self.font
let mode: NSLineBreakMode = self.lineBreakMode
let screenSize = UIScreen.main.bounds
let labelWidth: CGFloat = self.frame.size.width
let labelHeight: CGFloat = self.frame.size.height
let sizeConstraint = CGSize(width: labelWidth, height: CGFloat.greatestFiniteMagnitude)
if let myText = self.text {
let attributes: [AnyHashable: Any] = [NSAttributedString.Key.font: font]
let attributedText = NSAttributedString(string: myText, attributes: attributes as? [NSAttributedString.Key : Any])
let boundingRect: CGRect = attributedText.boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, context: nil)
if boundingRect.size.height > labelHeight {
var index: Int = 0
var prev: Int = 0
let characterSet = CharacterSet.whitespacesAndNewlines
repeat {
prev = index
if mode == NSLineBreakMode.byCharWrapping {
index += 1
} else {
index = (myText as NSString).rangeOfCharacter(from: characterSet, options: [], range: NSRange(location: index + 1, length: myText.count - index - 1)).location
}
} while index != NSNotFound && index < myText.count && (myText as NSString).substring(to: index).boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, attributes: attributes as? [NSAttributedString.Key : Any], context: nil).size.height <= labelHeight
return prev
}
}
if self.text == nil {
return 0
} else {
return self.text!.count
}
}
}
This is my tableView cell
//
// PostTableViewCell.swift
// SeeMore
//
// Created by Macbook Pro on 14/10/20.
//
import UIKit
protocol PostTableViewCellDelegate {
func postLabelAction(cell: PostTableViewCell, post: Post, tap: UITapGestureRecognizer)
}
class PostTableViewCell: UITableViewCell {
#IBOutlet weak var postLabel: UILabel!
var currentPost : Post?
var delegate: PostTableViewCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
postLabel.textColor = .black
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(postLabelSelected))
tapGesture.numberOfTapsRequired = 1
postLabel.isUserInteractionEnabled = true
postLabel.addGestureRecognizer(tapGesture)
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#objc func postLabelSelected(tap: UITapGestureRecognizer) {
delegate?.postLabelAction(cell: self, post: currentPost!, tap: tap)
}
func applyAttributedStringToPost(attributedString: NSMutableAttributedString, item: Post) {
let text = attributedString.string
let urls = text.extractURLs
for url in urls {
let range1 = (text as NSString).range(of: url.absoluteString)
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.blue, range: range1)
postLabel.attributedText = attributedString
}
postLabel.attributedText = attributedString
}
func setupItems(item : Post){
currentPost = item
let postText = item.postText
postLabel.numberOfLines = item.isExpendable ? 0 : 4
postLabel.text = postText
let underlineAttriString = NSMutableAttributedString(string: postText)
let urls = postText.extractURLs
for url in urls {
let range1 = (postText as NSString).range(of: url.absoluteString)
underlineAttriString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.blue, range: range1)
postLabel.attributedText = underlineAttriString
}
//Apply See more see less
postLabel.sizeToFit()
// let screenSize = UIScreen.main.bounds/
if let newfont = postLabel.font {
let textHeight = postText.textHeight(withConstrainedWidth: postLabel.frame.size.height, font: newfont)
if item.isExpendable {
postLabel.addTrailingForShowLess(with: "...", moreText: "Show Less", moreTextFont: newfont, moreTextColor: UIColor.blue) { (attributedString) in
self.applyAttributedStringToPost(attributedString: attributedString, item: item)
}
} else if postLabel.frame.size.height < textHeight, !item.isExpendable {
postLabel.addTrailing(with: "...", moreText: "Show More", moreTextFont: newfont, moreTextColor: UIColor.blue) { (attributedString) in
self.applyAttributedStringToPost(attributedString: attributedString, item: item)
}
}
}
}
}
This is my view controller
// ViewController.swift
// SeeMore
//
// Created by Macbook Pro on 14/10/20.
//
import UIKit
struct Post {
var postText = ""
var isExpendable = false
}
class ViewController: UIViewController {
var postArray : [Post] = [Post]()
#IBOutlet weak var postTableView: UITableView!{
didSet{
settingUpTableView()
}
}
override func viewDidLoad() {
super.viewDidLoad()
fillUpPostData()
postTableView.reloadData()
}
func settingUpTableView() {
let nib = UINib(nibName: "PostTableViewCell", bundle: nil)
postTableView.register(nib, forCellReuseIdentifier: "PostTableViewCell")
postTableView.rowHeight = UITableView.automaticDimension
postTableView.estimatedRowHeight = 300
postTableView.separatorStyle = .singleLine
postTableView.separatorInset = .zero
}
func fillUpPostData() {
postArray.append(Post(postText: "Next, we established questions. We usedโ“liberally to indicate questions, and ๐Ÿค” to indicate โ€œIโ€™m not understanding.โ€ Iโ€™d say questions composed about 30โ€“40% of our communication, so this was a critical emoji discovery.\nThen, we added more interesting complex relationships. ๐Ÿ”œ implied that, after time, one thing would lead to another. ๐Ÿค“๐Ÿ”œ๐Ÿ—ฃ๏ธ means that โ€œIโ€™ll be able to talk soon.โ€ We created a scale for asking โ€œHow do you feel?โ€: ๐Ÿ˜„๐Ÿ˜€๐Ÿ™‚๐Ÿ˜•โ˜น๏ธ๐Ÿ˜ดโ“\nAs our communication advanced, we adopted each othersโ€™ language. My friend used ๐Ÿ”บ to indicate โ€œand.โ€ After I understood, I started adopting the same emoji meaning. Since communication was so challenging and time-intensive, cooperating on each othersโ€™ emoji meaning was critical.\nTime in emoji is very expressive. ๐Ÿ•๐Ÿ•‘๐Ÿ•’๐Ÿ•“๐Ÿ•”๐Ÿ••๐Ÿ•–๐Ÿ•—๐Ÿ•˜๐Ÿ•™๐Ÿ•š๐Ÿ•›๐Ÿ•œ๐Ÿ•๐Ÿ•ž๐Ÿ•Ÿ๐Ÿ• ๐Ÿ•ก๐Ÿ•ข๐Ÿ•ฃ๐Ÿ•ค๐Ÿ•ฅ๐Ÿ•ฆ๐Ÿ•ง allowed us to communicate time very easily", isExpendable: false))
postArray.append(Post(postText: "Apple has just finished its โ€œHi, Speedโ€ event, where it finally took the wraps off the four new iPhone 12 phones, which have all-new designs and will all support 5G wireless networks. Apple also unveiled the HomePod mini, a smaller and more affordable version of the HomePod smart speaker.\nIf you want to read the play-by-play of the event, check out our live blog with commentary from Dieter Bohn and Nilay Patel. But if you just want to read the biggest news from the show, weโ€™ve got you covered right here.\nApple has just finished its โ€œHi, Speedโ€ event, where it finally took the wraps off the four new iPhone 12 phones, which have all-new designs and will all support 5G wireless networks. Apple also unveiled the HomePod mini, a smaller and more affordable version of the HomePod smart speaker.\nIf you want to read the play-by-play of the event, check out our live blog with commentary from Dieter Bohn and Nilay Patel. But if you just want to read the biggest news from the show, weโ€™ve got you covered right here....Show Less", isExpendable: false))
postArray.append(Post(postText: "Next, we established questions. We usedโ“liberally to indicate questions, and ๐Ÿค” to indicate \"Iโ€™m not understanding.\" Iโ€™d say questions composed about 30โ€“40% of our communication, so this was a critical emoji discovery.\nThen, we added more interesting complex relationships. ๐Ÿ”œ implied that, after time, one thing would lead to another. ๐Ÿค“๐Ÿ”œ๐Ÿ—ฃ๏ธ means that \"Iโ€™ll be able to talk soon.\" We created a scale for asking โ€œHow do you feel?\": ๐Ÿ˜„๐Ÿ˜€๐Ÿ™‚๐Ÿ˜•โ˜น๏ธ๐Ÿ˜ดโ“\nAs our communication advanced, we adopted each othersโ€™ language. My friend used ๐Ÿ”บ to indicate \"and.", isExpendable: false))
postArray.append(Post(postText: "BIG breaking news! Trump is going to have a fit over this one. โ€œA former Department of Homeland Security official questioned in a scathing op-ed how anyone could vote for President Donald Trump in the 2020 election, citing the chaos that has characterized his administration and his mishandling of the coronavirus pandemic.\nElizabeth Neumann explained in a USA Today column ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™โค๏ธ๐Ÿ™๐Ÿปโ˜ฆ๏ธ๐Ÿค˜๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘ published Tuesday why she is โ€œconvincedโ€ the president is โ€œfailing at keeping Americans safe.โ€ Neumann, until April, spent three years as a high-ranking member of the Trump administrationโ€™s national security team.\nโ€œHe is dangerous for our country,โ€ she wrote.\nNeumann cited the presidentโ€™s failure to address the surge in white nationalist violence, his constant lying and the turnover of key administration officials. Without elaborating, she also referred to what she said was a close call that could have led the U.S. into war.\n\nShe warned the countryโ€™s abandonment of allies and appeasement of dictators would โ€œonly get worseโ€ in a second Trump term. Her column is headlined:ย โ€œTrump made it hard for me to protect America. How could I vote for him again? How could anyone?โ€\n\nNeumann, ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™โค๏ธ๐Ÿ™๐Ÿปโ˜ฆ๏ธ๐Ÿค˜๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘ who endorsed Democratic nominee Joe Biden in a video released by the Republican Voters Against Trump group in August, had particularly harsh commentary on the pandemic.\n\nTrumpโ€™s intentional public dismissal of the threat of COVID-19 โ€” while acknowledging its โ€œdeadlyโ€ nature in private โ€” was worse than a โ€œdereliction of duty,โ€ said Neumann.\n\nโ€œYour government is supposed to perform some basic functions; keeping you and your family safe is primary among them,โ€ she wrote. โ€œIn 2016, I voted for President Trump. But when someone asked me if I could vote for him again, after he time and again refused to keep Americans safe โ€”ย how could I say anything but no? How could anyone?โ€ ", isExpendable: false))
postArray.append(Post(postText: " \n\n\nHere is her entire op-Ed: Everything we saw during the first presidential debate is indicative of how President Donald Trump behaves in the White House. His business model is chaos. ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™โค๏ธ๐Ÿ™๐Ÿปโ˜ฆ๏ธ๐Ÿค˜๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘He has no organization, no leadership, and sees every interaction as a contest or a battle, even when it doesnโ€™t have to be. Chris Wallace now knows how so many administration staffers feel โ€” and how I felt when the president got in the way of me doing my job. He is dangerous for our country.\n\nI served as the Assistant Secretary of Homeland Security for Counterterrorism and Threat Prevention, and my job was to help keep Americans safe from terrorist attacks. My time in office coincided with a dramatic rise in white nationalist violence, but my colleagues and I couldnโ€™t get the president to help address the problem. At the debate, America saw what I saw in the administration: President Trump refuses to distance himself from white nationalists. ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™โค๏ธ๐Ÿ™๐Ÿปโ˜ฆ๏ธ๐Ÿค˜๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘I realized after watching the White House response to the terrorist attack in El Paso that his rhetoric was a recruitment tool for violent extremist groups. The president bears some responsibility for the deaths of Americans at the hands of these violent extremists.\n\n\nAs a conservative, I believe a primary purpose of the federal government is to provide for the national defense. Under the Constitution, it is a mandatory function of the federal government. After serving for three years inside the Trump administrationโ€™s national security team, I am convinced the president is failing at keeping Americans safe.\n\nEarly on in the administration, I represented the Department of Homeland Security at several meetings in which a White House staff member implied that the president had approved, and that we should begin to carry out, plans that could have led the United States into war. Thankfully, there were experienced people in the room who had enough clout to suggest that these catastrophic plans needed a second look. Many of us werenโ€™t sure what, if anything, the president had actually approved, or that he had been properly briefed to ensure he understood the risks involved. These people helped us avoid war.", isExpendable: false))
postArray.append(Post(postText: "\n\nHaving adults in the room matters. They protect the country from a chaotic White House structure that allows staffers to run amok. But more importantly, they ensure that the president is presented with unvarnished truth, that difficult topics like domestic terrorism are raised even when he ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™โค๏ธ๐Ÿ™๐Ÿปโ˜ฆ๏ธ๐Ÿค˜๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘doesnโ€™t want to acknowledge them.\n\nEvery day, the number of experienced people in the administration that have the ability to speak truth to power shrinks. We are seeing the results: abandoning our allies and cozying up to dictators, emboldening our enemies and weakening our standing in the world. This would only get worse in a second Trump term.\n\nJust as he ignored white nationalists, he also ignored COVID-19. After nearly 20 years around national crises, you can recognize a good response โ€” and a bad one. By late January, professionals and outside experts were sounding the alarms. Health and Human Services Secretary Alex Azar declared a public health emergency on Jan. 31. But throughout February, longtime colleagues at the Federal ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™โค๏ธ๐Ÿ™๐Ÿปโ˜ฆ๏ธ๐Ÿค˜๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘Emergency Management Agency were privately expressing dismay at the flailing response.\n\nBob Woodwardโ€™s revelations last month, using the presidentโ€™s own words, demonstrate that the governmentโ€™s failure wasnโ€™t just because of bureaucracy, the newness of the virus, or even the president sticking his head in the sand. It was intentional. This is worse than dereliction of duty โ€” this is willfully leading defenseless people to a killer. President Trump repeatedly lied to the American public. Those lies have led to more deaths and illnesses.\n\nA recent study assessed that had we exercised our pandemic mitigation plans (which have existed since 2005), as other wealthy countries did, nearly 9 million more Americans would be employed, and over 100,000 would still be alive. We could have saved half of the Americans who have died so far.\n\nYour government is supposed to perform some basic functions; keeping you and your family safe is primary among them. In 2016, I voted for President Trump. But when someone asked me if I could vote for him again, after he time and again refused to keep Americans safe โ€” how could I say anything but no? How could anyone?...Show Less", isExpendable: false))
postArray.append(Post(postText: "\nPamela Lindsay:\nโ€just another conspiracy theory that proved to be untrue look at all the money he has wasted for something that was patently untrue. how many does this make that he yelled about some half baked ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™โค๏ธ๐Ÿ™๐Ÿปโ˜ฆ๏ธ๐Ÿค˜๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘conspiracy theory that just showed how desperate he is. yet look at the number that will still follow him he and they are truly deplorable.\nBeverly Jones: โ€Tax dollars wasted in another attempt to blame a former President of wrong-doing. Since we already know #45's wrongdoings, an investigation will cost nothing.", isExpendable: false))
postArray.append(Post(postText: "Judy Stamberger:\n \"AN incredible, 8 yrs served with dignity, kindness, no lawsuits, arrests, no impeachment, no crude remarks, respectful man & his family.\"\nJames S Lechleitnerjr:\n\"Outstanding men Martin Luther King, and President Barack Obama...\nAmenโ—๏ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™๐Ÿ’™โค๏ธ๐Ÿ™๐Ÿปโ˜ฆ๏ธ๐Ÿค˜๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘๐Ÿ‘โ€ผ๏ธ\"\nCarol Brown-Hatcher:\n\"Greatness, Grace, Integrity, Honorable(Men,) Brilliant, Humble, Brave, Courageous. THEY WOULD NOT BE SILENCED. They knew the meaning of \"Responsibility\".", isExpendable: false))
postArray.append(Post(postText: "BRAVO! ๐Ÿ‘ We love this!!! A North Dakota farmer is going viral for plowing a pro-Biden-Harris message into his field. Peter Larson says that this is the first time he has used his fields to share his political views. He hopes to encourage others to get out and vote. Great idea, Peter! ๐Ÿ‘๐Ÿ‘๐Ÿ‘ Follow Ridin' With Biden to defeat Trump and save America! ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ...Show Less", isExpendable: false))
postArray.append(Post(postText: "Ridin' With Biden:\n\"Olivia Troye, who until a couple of months ago was on Mike Pence's FAILED Coronavirus \"Response\" Task Force, just slammed Trump for PRETENDING that Dr. Fauci praised Trump's disastrous COVID response in a misleading campaign ad. Follow Ridin' With Biden to defeat Trump and save America!\" ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ๐Ÿ‡บ๐Ÿ‡ธ", isExpendable: false))
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return postArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PostTableViewCell", for: indexPath) as! PostTableViewCell
cell.setupItems(item: postArray[indexPath.row])
cell.delegate = self
return cell
}
}
extension ViewController: PostTableViewCellDelegate{
func postLabelAction(cell: PostTableViewCell,post: Post, tap: UITapGestureRecognizer) {
let tapLocation = tap.location(in: cell.postLabel)
let tapIndex = cell.postLabel.indexOfAttributedTextCharacterAtPoint(point: tapLocation, font: cell.postLabel.font!)
var rangeArray: [NSRange] = [NSRange]()
var linkRangeArray: [NSRange] = [NSRange]()
let postText = cell.postLabel.text
var seemoreText = ""
seemoreText = post.isExpendable ? "Show Less" : "Show More"
//
if let seeRange = postText?.range(of: seemoreText)?.nsRange {
if tapIndex > seeRange.location && tapIndex < seeRange.location + seeRange.length {
if let indexPath = postTableView.indexPath(for: cell) {
let isExpanded = self.postArray[indexPath.row].isExpendable
self.postArray[indexPath.row].isExpendable = !isExpanded
DispatchQueue.main.async { [weak self] in
self?.postTableView.reloadRows(at: [indexPath], with: .automatic)
self?.postTableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
return
}
}
}
if let urls = postText?.extractURLs {
for url in urls {
guard let range = postText?.range(of: url.absoluteString)?.nsRange else { return }
linkRangeArray.append(range)
}
for (index, neRange) in linkRangeArray.enumerated() {
if tapIndex > neRange.location && tapIndex < neRange.location + neRange.length {
print("link print")
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LinkPreviewWebViewController") as! LinkPreviewWebViewController
vc.loadURL = urls[index].absoluteString
self.navigationController?.isNavigationBarHidden = false
self.navigationController?.pushViewController(vc, animated: true)
return
}
}
}
// mention logic will be there
}
}
If there are emoji's or extra line gap before post text visibleTextLength return wrong count as a result users can't see the show more text because it is below visible text. even label.frame.size doesn't give me the current width of the different devices. Any help will be greatly appreciated. TIA
[Please ignore the post text as it's taken from real user to produce the problem]
It should be mentioned that i have tried Expendable Label and ReadMoreTextView but they can't serve me well.
This the output with the problem i'm facing
You can use https://github.com/apploft/ExpandableLabel this Class for show more/read more/more functionality in UILabel. This class is Customizable as per your requirements. I use this it will working fine.
I hope it will work for you.
Thanks.

UIFont: How to use Stylistic Alternate character?

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
}
}

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!

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)
}
}

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