Display japanese Text with furigana in UILabel

天涯浪子 提交于 2019-12-02 12:24:39

I have same issue.
Your code as Swift4 works correctly on iOS10. It does not function correctly on iOS11.
It seems that there was a change in NSAttributedString of UILabel on iOS 11.
I wrote a general program to draw vertical writing characters in CoreText before. In CoreText, NSAttributedString works on iOS11 as well. Although it is a provisional method, you can avoid this problem by using CoreText.
I written sample code. Since this draws characters directly to context, there is no need for UILabel, but draw with UILabel 's drawText for the moment.

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

And, Use it like below.

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var furiganaLabel: CustomLabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        furiganaLabel.attributedText = Utility.sharedInstance.furigana(String: "|優勝《ゆうしょう》の|懸《か》かった|試合《しあい》。")
    }
}

class CustomLabel: UILabel, SimpleVerticalGlyphViewProtocol {
    //override func draw(_ rect: CGRect) { // if not has drawText, use draw UIView etc
    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))
        drawContext(attributed, textDrawRect: rect, isVertical: isVertical)
    }
}

I added the CustomLabel class which adapted SimpleVerticalGlyphViewProtocol.
Please set the CustomLabel class as UILabel 's Custom Class on Storyboard.

Font size auto scale, and specify line number 0

Fixed font size, specify line number 4, baseline alignCenters, and last line truncated.

Font size auto scale, and specify line number 0 with Ruby

The above code works perfectly but there is one problem. Overriding the method drawText(in rect: CGRect), it loses every benefit of managing of text that is done by UILabel. In fact, the text isn't cut when the text goes out of size of the label.

this is the result of code:

CustomTableViewCell

    import UIKit

    class CustomTableViewCell: UITableViewCell {

        let userDefaults = UserDefaults.standard

        var japaneseKanji = ""{
            didSet{
            if japaneseKanji != oldValue {
                japaneseKanjiLabel.attributedText = Utility.sharedInstance.furigana(String: japaneseKanji)
            }
        }
    }

    var japaneseRomaji = ""{
        didSet{
            if japaneseRomaji != oldValue {
                japaneseRomajiLabel.text = japaneseRomaji
            }
        }
    }

    var italianText = ""{
        didSet{
            if italianText != oldValue {
                italianLabel.text = italianText
            }
        }
    }

    var englishText = ""{
        didSet{
            if englishText != oldValue {
                englishLabel.text = englishText
            }
        }
    }

    private var japaneseImage = UIImageView()
    private var romajiImage = UIImageView()
    private var italianImage = UIImageView()
    private var englishImage = UIImageView()

    private var japaneseKanjiLabel = GQAsianLabel()
    //private var japaneseKanjiLabel = UILabel()
    private var japaneseRomajiLabel = UILabel()
    private var italianLabel = UILabel()
    private var englishLabel = UILabel()

    override init(style: UITableViewCellStyle, reuseIdentifier: String?){
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        let japaneseImageRect = CGRect(x: 16, y: 14, width: 25, height: 25)
        japaneseImage = UIImageView(frame: japaneseImageRect)
        japaneseImage.translatesAutoresizingMaskIntoConstraints = false

        japaneseImage.image = UIImage(named: "jp")
        self.contentView.addSubview(japaneseImage)

        let romajiImageRect = CGRect(x: 16, y: 50, width: 25, height: 25)
        romajiImage = UIImageView(frame: romajiImageRect)
        romajiImage.translatesAutoresizingMaskIntoConstraints = false
        romajiImage.image = UIImage(named: "romaji")
        self.contentView.addSubview(romajiImage)

        let italianImageRect = CGRect(x: 16, y: 86, width: 25, height: 25)
        italianImage = UIImageView(frame: italianImageRect)
        italianImage.translatesAutoresizingMaskIntoConstraints = false
        italianImage.image = UIImage(named: "it")
        self.contentView.addSubview(italianImage)

        let japaneseKanjiLabelRect = CGRect(x: 62, y: 8, width: 280, height: 46)
        japaneseKanjiLabel = GQAsianLabel(frame: japaneseKanjiLabelRect)
        japaneseKanjiLabel.isVertical = false
        //japaneseKanjiLabel = UILabel(frame: japaneseKanjiLabelRect)
        japaneseKanjiLabel.numberOfLines = 0
        japaneseKanjiLabel.translatesAutoresizingMaskIntoConstraints = false
        japaneseKanjiLabel.font = UIFont(name: "YuKyo_Yoko-Medium", size: 17)
        japaneseKanjiLabel.sizeToFit()
        japaneseKanjiLabel.backgroundColor = UIColor.brown
        self.contentView.addSubview(japaneseKanjiLabel)

        let japaneseRomajiLabelRect = CGRect(x: 62, y: 52, width: 280, height: 21)
        japaneseRomajiLabel = UILabel(frame: japaneseRomajiLabelRect)
        japaneseRomajiLabel.textAlignment = .left
        japaneseRomajiLabel.numberOfLines = 0
        japaneseRomajiLabel.translatesAutoresizingMaskIntoConstraints = false
        japaneseRomajiLabel.font = UIFont(name: "HelveticaNeueLTPro-Lt", size: 14)
        self.contentView.addSubview(japaneseRomajiLabel)

        let italianLabelRect = CGRect(x: 62, y: 90, width: 280, height: 21)
        italianLabel = UILabel(frame: italianLabelRect)
        italianLabel.textAlignment = .left
        italianLabel.numberOfLines = 0;
        italianLabel.translatesAutoresizingMaskIntoConstraints = false
        italianLabel.font = UIFont(name: "HelveticaNeueLTPro-Lt", size: 14)
        self.contentView.addSubview(italianLabel)

        let englishImageRect = CGRect(x: 16, y: 122, width: 25, height: 25)
        englishImage = UIImageView(frame: englishImageRect)
        englishImage.translatesAutoresizingMaskIntoConstraints = false
        englishImage.image = UIImage(named: "en")
        self.contentView.addSubview(englishImage)

        let englishLabelRect = CGRect(x: 62, y: 138, width: 280, height: 21)
        englishLabel = UILabel(frame: englishLabelRect)
        englishLabel.textAlignment = .left
        englishLabel.numberOfLines = 0
        englishLabel.translatesAutoresizingMaskIntoConstraints = false
        englishLabel.font = UIFont(name: "HelveticaNeueLTPro-Lt", size: 13)
        self.contentView.addSubview(englishLabel)

        let englishLanguage = userDefaults.object(forKey: "EnglishLang") as! String
        let isThereEnglish = userDefaults.object(forKey: "isThereEnglish") as! String

        let viewDictionary = ["japaneseImage":japaneseImage, "romajiImage":romajiImage, "italianImage":italianImage, "englishImage":englishImage, "kanjiLabel":japaneseKanjiLabel, "romajiLabel":japaneseRomajiLabel, "italianLabel":italianLabel, "englishLabel":englishLabel] as [String : AnyObject]
        let japaneseImage_H = NSLayoutConstraint.constraints(withVisualFormat: "H:[japaneseImage(25)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        let japaneseImage_V = NSLayoutConstraint.constraints(withVisualFormat: "V:[japaneseImage(25)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        let japaneseImage_POS_H = NSLayoutConstraint.constraints(withVisualFormat: "H:|-16-[japaneseImage]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)

        let japaneseKanji_POS_H = NSLayoutConstraint.constraints(withVisualFormat: "H:[japaneseImage]-21-[kanjiLabel]-8-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        let japaneseKanji_POS_V = NSLayoutConstraint.constraints(withVisualFormat: "V:|-10-[kanjiLabel]-20-[romajiLabel]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        self.contentView.addConstraint(NSLayoutConstraint.init(item: japaneseImage, attribute: NSLayoutAttribute.centerY, relatedBy: NSLayoutRelation.equal, toItem: japaneseKanjiLabel, attribute: NSLayoutAttribute.centerY, multiplier: 1.0, constant: 0))

        let romajiImage_H = NSLayoutConstraint.constraints(withVisualFormat: "H:[romajiImage(25)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        let romajiImage_V = NSLayoutConstraint.constraints(withVisualFormat: "V:[romajiImage(25)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        let romajiImage_POS_H = NSLayoutConstraint.constraints(withVisualFormat: "H:|-16-[romajiImage]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)

        let romajiLabel_POS_H = NSLayoutConstraint.constraints(withVisualFormat: "H:[romajiImage]-21-[romajiLabel]-8-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        let romajiLabel_POS_V = NSLayoutConstraint.constraints(withVisualFormat: "V:[romajiLabel]-20-[italianLabel]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        self.contentView.addConstraint(NSLayoutConstraint.init(item: romajiImage, attribute: NSLayoutAttribute.centerY, relatedBy: NSLayoutRelation.equal, toItem: japaneseRomajiLabel, attribute: NSLayoutAttribute.centerY, multiplier: 1.0, constant: 0))

        let italianImage_H = NSLayoutConstraint.constraints(withVisualFormat: "H:[italianImage(25)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        let italianImage_V = NSLayoutConstraint.constraints(withVisualFormat: "V:[italianImage(25)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        let italianImage_POS_H = NSLayoutConstraint.constraints(withVisualFormat: "H:|-16-[italianImage]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)

        let italianLabel_POS_H = NSLayoutConstraint.constraints(withVisualFormat: "H:[italianImage]-21-[italianLabel]-8-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        self.contentView.addConstraint(NSLayoutConstraint.init(item: italianImage, attribute: NSLayoutAttribute.centerY, relatedBy: NSLayoutRelation.equal, toItem: italianLabel, attribute: NSLayoutAttribute.centerY, multiplier: 1.0, constant: 0))

        let englishImage_H = NSLayoutConstraint.constraints(withVisualFormat: "H:[englishImage(25)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        let englishImage_V = NSLayoutConstraint.constraints(withVisualFormat: "V:[englishImage(25)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        let englishImage_POS_H = NSLayoutConstraint.constraints(withVisualFormat: "H:|-16-[englishImage]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)

        let englishLabel_POS_H = NSLayoutConstraint.constraints(withVisualFormat: "H:[englishImage]-21-[englishLabel]-8-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
        self.contentView.addConstraint(NSLayoutConstraint.init(item: englishImage, attribute: NSLayoutAttribute.centerY, relatedBy: NSLayoutRelation.equal, toItem: englishLabel, attribute: NSLayoutAttribute.centerY, multiplier: 1.0, constant: 0))


        japaneseImage.addConstraints(japaneseImage_H)
        japaneseImage.addConstraints(japaneseImage_V)
        self.contentView.addConstraints(japaneseImage_POS_H)

        self.contentView.addConstraints(japaneseKanji_POS_H)
        self.contentView.addConstraints(japaneseKanji_POS_V)

        romajiImage.addConstraints(romajiImage_H)
        romajiImage.addConstraints(romajiImage_V)
        self.contentView.addConstraints(romajiImage_POS_H)

        self.contentView.addConstraints(romajiLabel_POS_H)
        self.contentView.addConstraints(romajiLabel_POS_V)

        italianImage.addConstraints(italianImage_H)
        italianImage.addConstraints(italianImage_V)
        self.contentView.addConstraints(italianImage_POS_H)

        self.contentView.addConstraints(italianLabel_POS_H)

        englishImage.addConstraints(englishImage_H)
        englishImage.addConstraints(englishImage_V)
        self.contentView.addConstraints(englishImage_POS_H)

        self.contentView.addConstraints(englishLabel_POS_H)

        if englishLanguage == "ON" && isThereEnglish == "True" {

            englishImage.alpha = 1.0
            englishLabel.alpha = 1.0

            let englishLabel_POS_V = NSLayoutConstraint.constraints(withVisualFormat: "V:[italianLabel]-20-[englishLabel]-10-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)

            self.contentView.addConstraints(englishLabel_POS_V)

        }else{
            englishImage.alpha = 0.0
            englishLabel.alpha = 0.0

            let italianLabel_POS_V = NSLayoutConstraint.constraints(withVisualFormat: "V:[italianLabel]-10-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
            self.contentView.addConstraints(italianLabel_POS_V)
        }
    }

    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
}

GQAsianLabel

    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 fontRef = UIFont(name: "Hiragino Sans", size: 17)
        attributed.addAttribute(kCTFontAttributeName as NSAttributedStringKey, value: fontRef!, range:NSMakeRange(0, attributed.length))

        let framesetter = CTFramesetterCreateWithAttributedString(attributed)
        let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributed.length), path, nil)

        CTFrameDraw(frame, context)
    }
}

class GQAsianLabel: UILabel, SimpleVerticalGlyphViewProtocol {
    var isVertical = false


    // Only override draw() if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    override func draw(_ rect: CGRect) {
        // Drawing code
        let attributed = NSMutableAttributedString(attributedString: self.attributedText!)
        //let isVertical = false // if Vertical Glyph, true.
        attributed.addAttributes([NSAttributedStringKey.verticalGlyphForm: isVertical], range: NSMakeRange(0, attributed.length))
        drawContext(attributed, textDrawRect: rect, isVertical: isVertical)
    }
 /*
    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))
        drawContext(attributed, textDrawRect: rect, isVertical: isVertical)
    }
 */
}

Utility class with furigana function

    import UIKit

extension String {
    func find(pattern: String) -> NSTextCheckingResult? {
        do {
            let re = try NSRegularExpression(pattern: pattern, options: [])
            return re.firstMatch(
                in: self,
                options: [],
                range: NSMakeRange(0, self.utf16.count))
        } catch {
            return nil
        }
    }

    func replace(pattern: String, template: String) -> String {
        do {
            let re = try NSRegularExpression(pattern: pattern, options: [])
            return re.stringByReplacingMatches(
                in: self,
                options: [],
                range: NSMakeRange(0, self.utf16.count),
                withTemplate: template)
        } catch {
            return self
        }
    }
}

class Utility: NSObject {
    class var sharedInstance: Utility {
        struct Singleton {
            static let instance = Utility()
        }
        return Singleton.instance
    }

    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 NSAttributedStringKey: annotation])
                    } else {
                        return NSAttributedString(string: x, attributes: nil)
                    }
                }
                .reduce(NSMutableAttributedString()) { $0.append($1); return $0 }

        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineHeightMultiple = 1
        paragraphStyle.lineSpacing = 0
        attributed.addAttribute(NSAttributedStringKey.paragraphStyle, value:paragraphStyle, range:NSMakeRange(0, (attributed.length)))

        return attributed
    }
}

Swift 4.2

I've found a solution at this link but there are some problems.

let me explain:

String Extensions

func find(pattern: String) -> NSTextCheckingResult? {
    do {
        let findRubyText = try NSRegularExpression(pattern: pattern, options: [])
        return findRubyText.firstMatch(
            in: self,
            options: [],
            range: NSMakeRange(0, self.utf16.count))
    } catch {
        return nil
    }
}

func replace(pattern: String, template: String) -> String {
    do {
        let replaceRubyText = try NSRegularExpression(pattern: pattern, options: [])
        return replaceRubyText.stringByReplacingMatches(
            in: self,
            options: [],
            range: NSMakeRange(0, self.utf16.count),
            withTemplate: template)
    } catch {
        return self
    }
}

These extensions let you to manipulate the string to add or remove the ruby annotation.

Ruby Methods

func rubyAttributedString(font: UIFont, textColor: UIColor) -> NSMutableAttributedString {
    let attributed =
        self.replace(pattern: "(|.+?《.+?》)", template: ",$1,")
            .components(separatedBy: ",")
            .map { x -> NSAttributedString in
                if let pair = x.find(pattern: "|(.+?)《(.+?)》") {
                    let baseText = (x as NSString).substring(with: pair.range(at: 1))
                    let ruby = (x as NSString).substring(with: pair.range(at: 2))

                    let rubyAttribute: [AnyHashable: Any] = [
                        kCTRubyAnnotationSizeFactorAttributeName: 0.5,
                        kCTForegroundColorAttributeName: textColor
                    ]

                    let annotation = CTRubyAnnotationCreateWithAttributes(.auto, .auto, .before, ruby as CFString, rubyAttribute as CFDictionary)

                    return NSAttributedString(
                        string: baseText,
                        attributes: [.font: font,
                                     .foregroundColor: textColor,
                                     kCTRubyAnnotationAttributeName as NSAttributedString.Key: annotation])

                } else {
                    return NSAttributedString(
                        string: x,
                        attributes: [.font: font,
                                     .foregroundColor: textColor]
                    )
                }
            }
            .reduce(NSMutableAttributedString()) { $0.append($1); return $0 }

    return attributed
}

func removeRubyString() -> String {
    return self.replace(pattern: "(|+|《.+?》)", template: "")
}

These methods let you inject the ruby annotation or remove the character for the ruby annotation.

draw(_:) Method. The true problem

To draw the attributed string that has the ruby annotation I'm trying to follow several example: - the site from which I take these updated methods link - Raywenderlich.com tutorial - documentation and answers of Stackoverflow

the problem is that some time that text is not displayed. maybe it shifted or out of bounds of label. other problem is that the label don't fit the text.

What I need is that: - the possibility to use Kyokasho type font principally, but all others types as well - the possibility to display horizontal and vertical text - very simply use:insert formatted string -> display text with phonetic guide and automatic size to fit the size of text without call any methods (e.g. myAsianLabel.text = "デーモン|小暮閣下《こぐれかっか》" myAsianLabel.otherMethodToResizeLabel() //<---- I don't want this)

Other Method

override var intrinsicContentSize: CGSize {
    let displayText: String = {
        if let unwrapText = self.text {
            return unwrapText
        }
        if let unwrapAttributedText = self.attributedText {
            return unwrapAttributedText.string
        }

        return ""
    }()

    let baseSize = displayText.removeRubyString().size(withAttributes: [.font: self.font])
    // ルビの分の高さを追加する
    var rubySize = CGSize()
    if isVertical == .horizontal{
        rubySize = CGSize(width: baseSize.width, height: baseSize.height + (self.font.pointSize * 0.75) / 2)
    }else{
        rubySize = CGSize(width:baseSize.height + (self.font.pointSize * 0.75), height: baseSize.width)
    }


    if let unwrapText = self.text, unwrapText != unwrapText.removeRubyString() {
        return rubySize
    }
    if let unwrapAttributedText = self.attributedText, unwrapAttributedText.string != unwrapAttributedText.string.removeRubyString() {
        return rubySize
    }

    return baseSize
}

This metodo seems that help to resize the label to fit the text but sometime don't resize well. For example with horizontal text and the text is log, don't show all lines and with vertical text the label is more large then the size of text.

Can you help me to create a label that has that feature? Once done, I would like to put in GitHub and share with everybody

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!