Search code examples
iosswiftuilabel

How to achieve left alignment on uilabel (same as suggestions appear in native keyboard)?


What I am trying to achieve (and I am not 100% sure how to do it or how to explain it properly) is described in the below screenshot.

I have added allowsDefaultTighteningForTruncation = true and lineBreakMode = .byClipping to my label, but it now displays the beginning of the word and I need to display the end of the word, any ideas on how to achieve that? or any ideas what to look for in apple docs? I've read everything I could think of so far.

enter image description here


Solution

  • To get that result, you need to embed the label in a UIView and constrain the label's Trailing but not Leading.

    Make sure the "holder" view has Clips To Bounds set to true.

    As the label grows in width, it will extend past the leading edge of the holder view.

    Here's a quick example:

    class ViewController: UIViewController {
    
        let theLabel = UILabel()
        let holderView = UIView()
        
        let strs: [String] = [
            "Misinterpret",
            "Misinterpreted",
            "Misinterpretation",
        ]
        var idx = 0
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            theLabel.translatesAutoresizingMaskIntoConstraints = false
            holderView.translatesAutoresizingMaskIntoConstraints = false
            
            holderView.backgroundColor = .systemBlue
            theLabel.backgroundColor = .yellow
            
            theLabel.font = .systemFont(ofSize: 30.0)
            
            holderView.addSubview(theLabel)
            view.addSubview(holderView)
            
            NSLayoutConstraint.activate([
                
                holderView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                holderView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                holderView.widthAnchor.constraint(equalToConstant: 200.0),
                holderView.heightAnchor.constraint(equalTo: theLabel.heightAnchor, constant: 8.0),
                
                theLabel.centerYAnchor.constraint(equalTo: holderView.centerYAnchor),
                theLabel.trailingAnchor.constraint(equalTo: holderView.trailingAnchor, constant: -8.0),
                
            ])
            
            // clip theLabel when it gets too wide
            holderView.clipsToBounds = true
            
            theLabel.text = strs[idx]
        }
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            idx += 1
            theLabel.text = strs[idx % strs.count]
        }
        
    }
    

    Output:

    enter image description here

    enter image description here

    enter image description here

    The "type ahead" suggestion bar probably also uses a gradient mask so the text does not look so abruptly clipped... but that's another question.


    Edit - here's a more complete example.

    • textField at the top
    • label in a gray "holder" view
    • green label showing actual size of text

    As you enter text, the labels will update.

    The label in the gray box will be centered horizontally, until it is too wide to fit, at which point it will stay "right-aligned." It will also have a slight gradient mask at the left edge so it is not cut off abruptly.

    class ViewController: UIViewController {
        
        let textField = UITextField()
        
        let theClippedLabel = UILabel()
        let holderView = UIView()
    
        // plain label showing the actual size
        let theActualLabel = UILabel()
    
        let leftEdgeFadeMask = CAGradientLayer()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            [textField, theClippedLabel, holderView, theActualLabel].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
            }
            
            holderView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            theClippedLabel.backgroundColor = .clear
            theActualLabel.backgroundColor = .green
            
            textField.borderStyle = .roundedRect
            textField.placeholder = "Type here..."
            textField.addTarget(self, action: #selector(didEdit(_:)), for: .editingChanged)
            
            theClippedLabel.font = .systemFont(ofSize: 30.0)
            theActualLabel.font = theClippedLabel.font
            
            holderView.addSubview(theClippedLabel)
            view.addSubview(holderView)
            view.addSubview(theActualLabel)
            view.addSubview(textField)
            
            // center label horizontally, unless it is wider than holderView (minus Left/Right "padding")
            let cx = theClippedLabel.centerXAnchor.constraint(equalTo: holderView.centerXAnchor)
            cx.priority = .defaultHigh
            
            NSLayoutConstraint.activate([
                
                // center a 200-pt wide "holder" view
                holderView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                holderView.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 20.0),
                holderView.widthAnchor.constraint(equalToConstant: 200.0),
                
                // holderView height is 16-pts taller than the label height (8-pts Top / Bottom "padding")
                holderView.heightAnchor.constraint(equalTo: theClippedLabel.heightAnchor, constant: 16.0),
                
                // center the label vertically
                theClippedLabel.centerYAnchor.constraint(equalTo: holderView.centerYAnchor),
                
                // keep the label's Trailing edge at least 8-pts from the holderView's Trailing edge
                theClippedLabel.trailingAnchor.constraint(lessThanOrEqualTo: holderView.trailingAnchor, constant: -8.0),
                
                // activate cx constraint
                cx,
                
                theActualLabel.topAnchor.constraint(equalTo: holderView.bottomAnchor, constant: 4.0),
                theActualLabel.centerXAnchor.constraint(equalTo: holderView.centerXAnchor),
                
                textField.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12.0),
                textField.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 12.0),
                textField.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12.0),
                
            ])
            
            // clip theLabel when it gets too wide
            holderView.clipsToBounds = true
            
            // gradient mask for left-edge of label
            leftEdgeFadeMask.colors = [UIColor.clear.cgColor, UIColor.black.cgColor]
            leftEdgeFadeMask.startPoint = CGPoint(x: 0.0, y: 0.0)
            leftEdgeFadeMask.endPoint = CGPoint(x: 1.0, y: 0.0)
            leftEdgeFadeMask.locations = [0.0, 0.1]
            theClippedLabel.layer.mask = leftEdgeFadeMask
            
            // so we have something to see when we start
            theClippedLabel.text = " "
            theActualLabel.text = theClippedLabel.text
        }
    
        @objc func didEdit(_ sender: Any) {
    
            // if the textField is empty, use a space character so
            //  the labels don't disappear
            var str = " "
            if let s = textField.text, !s.isEmpty {
                str = s
            }
            theClippedLabel.text = str
            theActualLabel.text = str
            updateMask()
    
        }
        
        func updateMask() -> Void {
            
            // update label frame
            theClippedLabel.sizeToFit()
            
            // we want the gradient mask to start at the leading edge
            //  of the holder view, with
            //  4-pts Left and 8-pts Right "padding"
            var r = holderView.bounds
            
            let targetW = r.width - 12
            r.size.width -= 12
            r.size.height -= 16
            r.origin.x = theClippedLabel.bounds.width - targetW
            
            // disable built-in layer animations
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            
            leftEdgeFadeMask.frame = r
            
            CATransaction.commit()
        }
        
    }
    

    Example result:

    enter image description here

    enter image description here

    enter image description here

    enter image description here

    Note that this is example code only. In practical use, we'd want to build this as a custom view with all of the sizing and gradient mask logic self-contained.