Is there any way to change text layout begin position in iOS with core text?

51 views Asked by At

I have a string : 12345678901234567890123456789012345678901234567890(...)

the default label will be layouting from left to right. enter image description here

and I want to display this label with this kind of layout (and the will be no ellipsis):

enter image description here

  1. the text layout begin not in the left , but starts in the middle

  2. then the layout continue to the right

  3. fill the left at last

How to do this

1

There are 1 answers

0
DonMag On BEST ANSWER

You can do this a few different ways...

  • two "normal" UILabel as subviews
  • CoreText
  • using two CATextLayer

Here's an example using CATextLayer. Based on your descriptions:

  • text begins at horizontal center, then "wraps" around to the left
  • no ellipses truncation
  • uses intrinsic size of the text/string

I'm calling the custom view subclass RightLeftLabelView:

class RightLeftLabelView: UIView {
    
    public var text: String = ""
    {
        didSet {
            setNeedsLayout()
            invalidateIntrinsicContentSize()
        }
    }
    public var font: UIFont = .systemFont(ofSize: 17.0)
    {
        didSet {
            setNeedsLayout()
            invalidateIntrinsicContentSize()
        }
    }
    public var textColor: UIColor = .black
    {
        didSet {
            setNeedsLayout()
        }
    }

    private let leftTL = CATextLayer()
    private let rightTL = CATextLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        clipsToBounds = true
        [leftTL, rightTL].forEach { tl in
            tl.contentsScale = UIScreen.main.scale
            layer.addSublayer(tl)
        }
    }
    override func layoutSubviews() {
        super.layoutSubviews()
    
        // get the size of the text, limited to a single line
        let sz = font.sizeOfString(string: text, constrainedToWidth: .greatestFiniteMagnitude)
        var r = CGRect(origin: .zero, size: CGSize(width: ceil(sz.width), height: ceil(sz.height)))
        // start right text layer at horizontal center
        r.origin.x = bounds.midX
        rightTL.frame = r //.offsetBy(dx: r.width * 0.5, dy: 0.0)
        // end left text layer at horizontal center
        r.origin.x -= r.width
        leftTL.frame = r //.offsetBy(dx: -r.width * 0.5, dy: 0.0)
        [leftTL, rightTL].forEach { tl in
            tl.string = text
            tl.font = font
            tl.fontSize = font.pointSize
            tl.foregroundColor = textColor.cgColor
        }
    }
    override var intrinsicContentSize: CGSize {
        return font.sizeOfString(string: text, constrainedToWidth: .greatestFiniteMagnitude)
    }
}

It uses this UIFont extension to get the size of the text:

extension UIFont {
    func sizeOfString (string: String, constrainedToWidth width: Double) -> CGSize {
        let attributes = [NSAttributedString.Key.font:self]
        let attString = NSAttributedString(string: string,attributes: attributes)
        let framesetter = CTFramesetterCreateWithAttributedString(attString)
        return CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(location: 0,length: 0), nil, CGSize(width: width, height: .greatestFiniteMagnitude), nil)
    }
}

and an example controller:

class RightLeftLabelVC: UIViewController {
    
    let sampleStrings: [String] = [
        "0123456789",
        "This is a good test of custom wrapping.",
        "This is a good test of custom wrapping when the text is too long to fit.",
    ]
    var sampleIDX: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let stack: UIStackView = {
            let v = UIStackView()
            v.axis = .vertical
            v.alignment = .center
            v.spacing = 2
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        view.addSubview(stack)
        
        let safeG = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            // center the stack view
            stack.centerYAnchor.constraint(equalTo: safeG.centerYAnchor),
            // full width
            stack.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
            stack.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
        ])
        
        let myTestViewA = RightLeftLabelView()
        let myTestViewB = RightLeftLabelView()
        let myTestViewC = RightLeftLabelView()
        
        let actualA = UILabel()
        let actualB = UILabel()
        let actualC = UILabel()

        stack.addArrangedSubview(infoLabel("UILabel"))
        stack.addArrangedSubview(actualA)
        stack.addArrangedSubview(infoLabel("Custom View"))
        stack.addArrangedSubview(myTestViewA)

        stack.addArrangedSubview(infoLabel("UILabel"))
        stack.addArrangedSubview(actualB)
        stack.addArrangedSubview(infoLabel("Custom View"))
        stack.addArrangedSubview(myTestViewB)
        
        stack.addArrangedSubview(infoLabel("UILabel"))
        stack.addArrangedSubview(actualC)
        stack.addArrangedSubview(infoLabel("Custom View"))
        stack.addArrangedSubview(myTestViewC)


        // some vertical spacing
        stack.setCustomSpacing(32.0, after: myTestViewA)
        stack.setCustomSpacing(32.0, after: myTestViewB)
        stack.setCustomSpacing(32.0, after: myTestViewC)

        // for convenience
        let rlViews: [RightLeftLabelView] = [
            myTestViewA, myTestViewB, myTestViewC
        ]
        let labels: [UILabel] = [
            actualA, actualB, actualC
        ]
        let strings: [String] = [
            "0123456789",
            "This is a good test of custom wrapping.",
            "This is an example test of custom wrapping when the text is too long to fit.",
        ]
        // set various properties
        var i: Int = 0
        for (v, l) in zip(rlViews, labels) {
            v.backgroundColor = .cyan
            v.text = strings[i]
            l.backgroundColor = .green
            l.text = strings[i]
            l.numberOfLines = 0
            i += 1
        }

    }
    
    func infoLabel(_ s: String) -> UILabel {
        let v = UILabel()
        v.text = s
        v.font = .italicSystemFont(ofSize: 14.0)
        return v
    }
}

The result looks like this:

enter image description here