Group Group Group Group Group Group Group Group Group

Label with Ruby Annotation

Hi everybody,
for my app I need a label that can shows a text with ruby annotation.
I’ve found this site. unfortunately an other Apple update makes this code useless, because ViewController calculates the cell size based on the text entered in the heightForRowAt method. But this method is calculated before building the cell and is therefore always returned 44. so if the text is in a row it is fine. but if it is in multiple lines only the first line is seen.

can you help me to fix to bug?

Please!!!

Hi @rufy, if you could share code and or snippets of code that you have tried that may help us better understand the question.

Hi @gdelarosa. in the bottom of the site there is the code to download that I used.

This project is a label in which is applied the information of the site.
apart from the fact that it does not change the orientation of the text from horizontal to vertical.
but the bigger problem is that if you have a long string, the cell don’t become bigger. because in ViewController the method heightForRowAt is called when the cell has not yet been created.

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    if let cell = tableView.cellForRow(at: indexPath) as? RubyTableViewCell {
        let attributed = cell.rubyLabel.text!.rubyAttributedString(font: cell.rubyLabel.font, textColor: cell.rubyLabel.textColor)
        // cellのラベルの上下マージンとして24を足す
        return attributed.boundingRect(with: CGSize(width: cell.rubyLabel.frame.width, height: .greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).size.height + 24  //if let return nil and goes in the else  
    } else {
        return 44
    }
}

Any suggestion to fix this problem?

p.s. the fact that the label is put in a cell, is great. for my app I need to put the label in cell (both tableView and CollectionView)

Thanks for sharing your code @rufy. If you are using storyboard you could possibly try setting the label to accept a value of 0 in the “Lines” option within the Attributes Inspector.

@gdelarosa thank you for your answer but doesn’t work. because, as I said the problem is that the method of table view, heightForRowAt, is called when the cells are not yet created.

Hi, @rufy,

as far as I understood, you need to have cells of various heights, depending on the contents. For that you do not need to use tableView(_:heightForRowAt:) method at all.

Could you please try the following:

  1. where you configure you tableVIew (e.g. viewDidLoad) add these lines:

     tableView.rowHeight = UITableView.automaticDimension
     tableView.estimatedRowHeight = 44
    
  2. as you have a custom cell RubyTableViewCell, then add these properties to a label:

     label.numberOfLines = 0
     label.lineBreakMode = .byWordWrapping
    

Hope this will help you.

hi @sobolevsky, thank you for your answer but, as I already said: it doesn’t work.

try with this datasource:
private let dataSource = [“デーモン|小暮閣下《こぐれかっか》”,
“エース|清水長官《しみずちょうかん》”,
“|怪人松崎様《かいじんまつざきさま》”,
“「まさか、|後罪《クライム》の|触媒《カタリスト》を〈|讃来歌《オラトリオ》〉無しで?」教師たちの狼狽した声が次々と上がる。”]

please, before launching random solutions, test the code with your solutions. then propose the solution that works. if it were solutions like yours, I would not have written this post. but since CoreText and UILabel’s Draw (_: CGRect) method are involved, it is more complicated.

Thank you for you patience

Hi @rufy .

Could you please provide a sample with your implementation?

@sobolevsky
ok. this is the code with my implementation.

REPEAT: this code has CoreText and is been overridden the draw method.RubySample.zip (76.8 KB)

thank you

hi guys,

I’ve implemented a label that does his work but with problems:

  1. when the text is long the context isn’t calculate well: it more big. it doesn’t fit to text
  2. is need to call a method to calculate the hight of label to show text
  3. in console came out some error about the constraints around of label

here is the code:

import UIKit
    public enum TextOrientation{
        case vertical
        case horizontal
    }
    class RubyLabel: UILabel {
        var orientation:TextOrientation = .horizontal
        
        lazy var attributed: NSMutableAttributedString = Utility.furigana(String: self.text!)
        var height = CGFloat()
        
        func heightOfCoreText() -> CGFloat {
            // initialize height and attributed
            height = CGFloat()
            attributed =  Utility.furigana(String: self.text!)
            attributed.addAttributes([NSAttributedString.Key.font:self.font], range:NSMakeRange(0, attributed.length))
            if orientation == .vertical {
                attributed.addAttribute(NSAttributedString.Key.verticalGlyphForm, value: true, range: NSMakeRange(0, attributed.length))
            }
            // MEMO: height = CGFloat.greatestFiniteMagnitude
            let textDrawRect = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: self.frame.size.width, height: CGFloat.greatestFiniteMagnitude)
            let framesetter = CTFramesetterCreateWithAttributedString(attributed)
            let frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, attributed.length), nil, textDrawRect.size, nil)
            height = frameSize.height
            return height
        }
        
        override func drawText(in rect: CGRect) {
            attributed.addAttributes([NSAttributedString.Key.font:self.font], range:NSMakeRange(0, attributed.length))
            attributed.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.black , range: NSMakeRange(0, attributed.length))
            
            if self.textAlignment == .center {
                var alignment = CTTextAlignment.center
                let alignmentSetting = [CTParagraphStyleSetting(spec: .alignment, valueSize: MemoryLayout.size(ofValue: alignment), value: &alignment)]
                let paragraphStyle = CTParagraphStyleCreate(alignmentSetting, 1)
                CFAttributedStringSetAttribute(attributed, CFRangeMake(0, CFAttributedStringGetLength(attributed)), kCTParagraphStyleAttributeName, paragraphStyle)
            }
            
            if orientation == .vertical {
                attributed.addAttribute(NSAttributedString.Key.verticalGlyphForm, value: true, range: NSMakeRange(0, attributed.length))
                let textDrawRect = CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.size.width, height: rect.size.height)
                drawContext(attributed, textDrawRect: textDrawRect, isVertical: true)
            }else{
                attributed.addAttribute(NSAttributedString.Key.verticalGlyphForm, value: false, range: NSMakeRange(0, attributed.length))
                let textDrawRect = CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.size.width, height: height)
                drawContext(attributed, textDrawRect: textDrawRect, isVertical: false)
            }
            self.backgroundColor = Style.white
            
            //let textDrawRect = CGRect(x: rect.origin.x, y: rect.origin.y, width: height , height:rect.size.width)
           // drawContext(attributed, textDrawRect: textDrawRect, isVertical: vertical!)
        }
        
        func drawContext(_ attributed:NSMutableAttributedString, textDrawRect:CGRect, isVertical:Bool) {
            
            guard let context = UIGraphicsGetCurrentContext() else { return }
            
            var path:CGPath
            if isVertical {
                context.rotate(by: .pi / 2)
                context.scaleBy(x: 1.0, y: -1.0)
                path = CGPath(rect: CGRect(x: textDrawRect.origin.y, y: textDrawRect.origin.x, width: textDrawRect.height, height: textDrawRect.width), transform: nil)
            }
            else {
                context.textMatrix = CGAffineTransform.identity
                context.translateBy(x: 0, y: textDrawRect.height)
                context.scaleBy(x: 1.0, y: -1.0)
                path = CGPath(rect: textDrawRect, transform: nil)
            }
            
            let framesetter = CTFramesetterCreateWithAttributedString(attributed)
            let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributed.length), path, nil)
            
            CTFrameDraw(frame, context)
        }
    }

class func furigana(String:String) -> NSMutableAttributedString {
        let attributed =
            String
                .replace(pattern: "(|.+?《.+?》)", template: ",$1,")
                .components(separatedBy: ",")
                .map { x -> NSAttributedString in
                    if let pair = x.find(pattern: "|(.+?)《(.+?)》") {
                        let string = (x as NSString).substring(with: pair.range(at: 1))
                        let ruby = (x as NSString).substring(with: pair.range(at: 2))
                        
                        var text: [Unmanaged<CFString>?] = [Unmanaged<CFString>.passRetained(ruby as CFString) as Unmanaged<CFString>, .none, .none, .none]
                        
                        let annotation = CTRubyAnnotationCreate(CTRubyAlignment.auto, CTRubyOverhang.auto, 0.5, &text[0])
                        
                        return NSAttributedString(
                            string: string,
                            attributes: [kCTRubyAnnotationAttributeName as NSAttributedString.Key: annotation])
                    } else {
                        return NSAttributedString(string: x, attributes: nil)
                    }
                }
                .reduce(NSMutableAttributedString()) { $0.append($1); return $0 }
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineHeightMultiple = 1.0
        paragraphStyle.minimumLineHeight = 5.0
        
        return attributed
    } 


as you can see, the screenshot show that the label is highter than the text.
Can you help me to fix these problems?

Hi @rufy,
You are drawing the text yourself, if instead you let the label autosize to multiple rows based on the text, that will give you a better experience instead.

First, Apple has intrinsic sizes and this helps in getting auto sizing elements like the label you want. To achieve this you need to set the constraints properly. So if you set the constraints of your UI to allow the label to grow as required, that will work. In this case it is a tableCell and if you set the constraints on the text label then it will autosize, you might have to add the numberOfLines = 0 so that it tries to break the sentence.

cheers,

@jayantvarma thank you very much for you answer. but your solution doesn’t work. because in iOS 10 Apple has delete the UILabel’s function that shown an attributed string with ruby annotation. if you try to give to UILabel the NSAttributedString created by function that injects the ruby annotation, you can see the string but without ruby annotation.
Moreover, it was Apple’s own engineers who told me that I had to create a UILable that would draw that particular text. because they wouldn’t have added the function again

hi @rufy, hmmmm tough one it is. well seems that you have it all sorted and know what to do. hope it works for you.

cheers,

@jayantvarma I’m sorry, but it’s not correct. I 've no solution for problems that I’ve written in this post. as I said your solution doesn’t work. so I’ve not received solution for the problems.

If somebody can help me, thank you.

Hi @rufy,

I’m also working on a project that needs display the ruby annotation. I took a look into your sample project, and have some ideas about your problem. I noticed that you use this line to get the baseSize.

displayText.removeRubyString().size(withAttributes: [.font: font])

But this method doesn’t consider about the width of the view. Instead, you can use boundingRect(with:options:attributes:context:) to calculate the size needed with the specified width.

let baseSize = (displayText.removeRubyString() as NSString).boundingRect(
    with: CGSize(width: super.intrinsicContentSize.width, height: 0),
    options: .usesLineFragmentOrigin,
    attributes: [.font: font], context: nil).size

This is the result of using the above code. I also removed tableView(_:heightForRowAt:) in the view controller because tableView.rowHeight is already set to UITableView.automaticDimension.

p.s. I’m actually not sure if there is any performance concern on using boundingRect(with:options:attributes:context:). Maybe there are better ways to get the size, the point I wanna mention here is that width does matter :slight_smile: