ioscore-text

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


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


Solution

  • You can do this a few different ways...

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

    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