0

Using the help of @yaslam I've created in Core Text an UILabel that show Japanese text in both horizontal and vertical way with Furigana using CTRubyAnnotation. Unfortunately I've a problem. I need to use this label inside a custom cell and I need that the cell dynamically resize the height of the cell based on text. but don't work. the cell doesn't expands

Can you help me?

Thank you very much

Here's code

import UIKit

protocol SimpleVerticalGlyphViewProtocol {
}

extension SimpleVerticalGlyphViewProtocol {

    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 CustomLabel: UILabel, SimpleVerticalGlyphViewProtocol {

    /*
    // Only override draw() if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    override func draw(_ rect: CGRect) {
        // Drawing code
    }
    */

    override func drawText(in rect: CGRect) {
        let attributed = NSMutableAttributedString(attributedString: self.attributedText!)
        let isVertical = false // if Vertical Glyph, true.
        attributed.addAttributes([NSAttributedStringKey.verticalGlyphForm: isVertical], range: NSMakeRange(0, attributed.length))
        attributed.addAttribute(NSAttributedStringKey.font, value: UIFont(name: "Hiragino Mincho ProN", size: 27)!, range: NSMakeRange(0, attributed.length))
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineHeightMultiple = 2
        paragraphStyle.lineSpacing = 4
        attributed.addAttribute(NSAttributedStringKey.paragraphStyle, value:paragraphStyle, range:NSMakeRange(0, (attributed.length)))
        drawContext(attributed, textDrawRect: rect, isVertical: isVertical)
    }
}

TableView class

import UIKit

class TableViewController: UITableViewController {

override func viewDidLoad() {
    super.viewDidLoad()

    // Uncomment the following line to preserve selection between presentations
    // self.clearsSelectionOnViewWillAppear = false

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

// MARK: - Table view data source

override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 1
}


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)

    let label = cell.viewWithTag(10) as! CustomLabel
    let attributedText = Utility.sharedInstance.furigana(String: "|銀行《ぎんこう》と|郵便局《ゆうびんきょく》の|間《あいだ》の|道《みち》をまっすぐ|行《い》くと、|学校《がっこう》の|前《まえ》に|出《で》ます。")


    label.attributedText = attributedText

    return cell
}

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {

    return UITableViewAutomaticDimension
    //return 70
}

this is the result:

without font attribute. The text is in two lines but the second is clipping. You can see if set the height of cell to 70 or more

Gabriele Quatela
  • 190
  • 3
  • 21

1 Answers1

1

Using CoreText, the height of the View is not automatically determined. Calculate drawing height of CoreText and set it to height of UIView in Cell. Make the following settings for UITableView in storyboard.
* Check Automatic of Row Height
* Check Automatic of Estimate

For programs, it is as follows.

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = UITableViewAutomaticDimension

Sample code to calculate drawing height of CoreText. I don't know whether it is the optimum code for calculating height. Sample code is pretty sloppy so please actually refactor it.

import UIKit

class CoreTextWithTableViewController: UITableViewController {

    var texts = [String]()

    override func viewDidLoad() {
        super.viewDidLoad()

        // tableView.rowHeight = UITableViewAutomaticDimension // or check Automatic of Row Height in storyboard
        // tableView.estimatedRowHeight = UITableViewAutomaticDimension // or check Automatic of Estimate in storyboard

        let text = "すでに|世界《せかい》で最も|先進的《せんしんてき》なモバイルオペレーティングシステムであるiOSに、iOS 11が新あらたな|基準《きじゅん》を打ち立てます。iPhoneは今まで以上に優れたものになり、iPadはかつてないほどの|能力《のうりょく》を手に入れます。さらにこれからはどちらのデバイスにも、ゲームやアプリケーションの|拡張現実《かくちょうげんじつ》のための驚くような|可能性《かのうせい》が広がります。iOS 11を|搭載《とうさい》するiPhoneとiPadは、間違いなくこれまでで最もパワフルで、最もパーソナルで、最も賢いデバイスです。"
        let text2 = "すでに|世界《せかい》で最も|先進的《せんしんてき》なモバイルオペレーティングシステムであるiOSに、iOS 11が新あらたな|基準《きじゅん》を打ち立てます。iPhoneは今まで以上に優れたものになり、iPadはかつてないほどの|能力《のうりょく》を手に入れます。"

        for _ in 0...20 {
            texts.append(text)
            texts.append(text2)
        }

        NotificationCenter.default.addObserver(self, selector: #selector(changeDirection(notification:)), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)

    }

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return texts.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCellWithCoreText
        let text = texts[indexPath.row]
        cell.coreTextView.text = text
        let height = cell.coreTextView.heightOfCoreText()
        cell.heightOfCoreTextView.constant = height

        // Execute redraw
        cell.coreTextView.setNeedsDisplay()  

        return cell
    }
}

extension CoreTextWithTableViewController {
    @objc func changeDirection(notification: NSNotification){
        tableView.reloadData()
    }
}



class CustomCellWithCoreText: UITableViewCell {
    @IBOutlet weak var coreTextView: CustomViewWithCoreText!
    @IBOutlet weak var heightOfCoreTextView: NSLayoutConstraint!
}

class CustomViewWithCoreText: UIView, SimpleVerticalGlyphViewProtocol {

    var text: String = ""
    lazy var attributed: NSMutableAttributedString = text.attributedStringWithRuby()
    var height = CGFloat()

    override func draw(_ rect: CGRect) {
        let textDrawRect = CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.size.width, height: height)
        drawContext(attributed, textDrawRect: textDrawRect, isVertical: false)
    }

    /// get Height of CoreText Draw Rect
    func heightOfCoreText() -> CGFloat {

        // initialize height and attributed
        height = CGFloat()
        attributed = text.attributedStringWithRuby()


        // 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 path = CGPath(rect: textDrawRect, transform: nil)
        let framesetter = CTFramesetterCreateWithAttributedString(attributed)
        let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributed.length), path, nil)
        let anyArray: [AnyObject] = CTFrameGetLines(frame) as [AnyObject]
        let lines = anyArray as! [CTLine]
        for line in lines {
            var ascent = CGFloat()
            var descent = CGFloat()
            var leading = CGFloat()
            CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
            height += ceil(ascent + descent + leading)
        }
        return height
    }
}

sample image
rotate tableView

It seems that the setting value of CTParagraphStyle is not reflected in the height obtained by CTLineGetTypographicBounds. Instead, using CTFramesetterSuggestFrameSizeWithConstraints works.

func heightOfCoreText() -> CGFloat {
    // initialize height and attributed
    height = CGFloat()
    attributed = text.attributedStringWithRuby()

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

minimumLineSpacing = 10.0
lineHeightMultiple = 1.5

use CTFramesetterSuggestFrameSizeWithConstraints

yaslam
  • 108
  • 7
  • Thank you very much for this answer. it's great, but there is only the last problem: when you rotate in landscape the device the string is stretched. I've tried substitute the uiview with uilabel and the method draw(in:) with drawText(in:). Work, but the cell don't resize because the constraints doesn't change. how can I fix this last problem? – Gabriele Quatela Dec 06 '17 at 22:52
  • @GabrieleQuatela I'm sorry, there was a problem in code when increased the number of data or view rotating. It was necessary to redraw when the cell was reused or view rotated. Therefore, I added setNeedsDisplay(). When view will rotate, call tableView.reloadData(). This will call "cellForRowAt indexPath" and execute setNeedsDisplay() again. – yaslam Dec 07 '17 at 08:27
  • thank you very much for your help. I've adaptive the problem to my situation and it's work. now I've some problem with the NSMutableParagraphStyle. if I set for example lineHeightMultiple = 1.1 the height isn't calculated well. and you can see only one line. but maybe it's because I don't know how it work. but it's ok. – Gabriele Quatela Dec 08 '17 at 22:50
  • @GabrieleQuatela It seems that the setting value of CTParagraphStyle is not reflected in the height obtained by CTLineGetTypographicBounds. Instead, using CTFramesetterSuggestFrameSizeWithConstraints works. However, in the past I did not work well when I used CTFramesetterSuggestFrameSizeWithConstraints to get the drawing range. Please also refer to the following post. [How to get the real height of text drawn on a CTFrame](https://stackoverflow.com/questions/8377496/how-to-get-the-real-height-of-text-drawn-on-a-ctframe) – yaslam Dec 11 '17 at 04:36
  • Thank you very much for your help. And I'm using in my app and it work very well. But I've a last question: I've noted that the text is without a leading and if I don't set the paragraphStyle is attached to top and bottom. there is a way to set some space in point around the text? Or because setting the lineHeightMultiple there is some space on the top, there is a way to set the space on the left and on the bottom? Thank you – Gabriele Quatela Dec 21 '17 at 20:57
  • @GabrieleQuatela If you want to add leading, trailing, top, bottom around the entire text, simply change the drawing size. If you want leading and trailing are set by CTParagraphStyle, it is possible with firstLineHeadIndent and headIndent and tailIndent. Use minimumLineHeight or lineSpacingAdjustment if you want line spacing without spaces above the text. Do not use lineHeightMultiple. [CTParagraphStyleSpecifier.](https://developer.apple.com/documentation/coretext/ctparagraphstylespecifier) I think that fine adjustment can be done by taking CTLine one by one and customizing it by oneself. – yaslam Dec 24 '17 at 11:20
  • Thank you @yaslam. I'm try to applicate you answer. but there are two problem. 1) how can I use the variable to switch from horizontal to vertical and vice versa? 2) your code don't use vertical glyph 3) If the size of the label is fixed, how can I use you code? How can I fix them? – Gabriele Quatela Dec 26 '17 at 12:26
  • @GabrieleQuatela 1), 2) About vertical text I have already answered you by the following question. you just use "NSAttributedStringKey.verticalGlyphForm". [Swift 4 CTRubyAnnotation don't work](https://stackoverflow.com/questions/46690337/swift-4-ctrubyannotation-dont-work) – yaslam Jan 05 '18 at 13:30
  • 3) A1: Truncate the text. I answerd below question about truncate tail of vertical text. [Multi-line NSAttributedString with truncated text](https://stackoverflow.com/questions/7611816/multi-line-nsattributedstring-with-truncated-text) A2: Reduce the font size. In this case, I will gradually reduce the font size and calculate by myself to the size fit in the drawing area. This is very simple, but the processing efficiency is very bad. Sorry, I don't know an efficient way to scale font size in CoreText. [refer here](https://qiita.com/yusuga/items/2be8c55ca561bba44702) – yaslam Jan 05 '18 at 13:31