1

I have a tableView cell, which consists of label and a textField.

textField has a width constraint equal to 150 and its text set to systemBold 30 with Adjust to fit turned on and minimum size as 10 in a Storyboard.

When textField begins editing I increase the textField width to 300 to give more space and animate it, using UIView.animate. Then back to initial state when editing was ended:

    func textFieldDidBeginEditing(_ textField: UITextField) {
        UIView.animate(withDuration: 1) { [self] in
            textFieldWidth.constant = 300
            self.layoutIfNeeded()
        }
    }
    
    func textFieldDidEndEditing(_ textField: UITextField) {
        UIView.animate(withDuration: 1) { [self] in
            textFieldWidth.constant = 150
            self.layoutIfNeeded()
        }
    }

The problem I have is that textField's text font changes to a bigger 30 size without any animation, straight away. And the same happens when textField goes back to its initial 150 width.

I tried different approaches like UIView.transition, transform with scaleX and scaleY, but it didn't work out. For example if I use transform and scale only by x, the text will be ugly shrinked. I need to find a way to save the text aspect ratio while animating.

I expect the font size changes (from small to 30) to be animated along with the changes of textField's width, starting from trailing constraint to left direction.

If it's not possible to do with a textField, maybe there is some workarounds with fake label or something, so I can show user smooth text size change?

How to implement such behavior?

Here is a gist with test project: CLICK

artexhibit
  • 198
  • 3
  • 17
  • upload full project and not like what you have added... – Fahim Parkar Mar 19 '23 at 13:15
  • Hei @FahimParkar. Uploaded the full project https://github.com/artexhibit/CustomTextFieldFontChange – artexhibit Mar 19 '23 at 13:19
  • https://stackoverflow.com/questions/46536530/how-to-animate-uilabel-text-size-and-color – Wo_0NDeR ᵀᴹ Mar 20 '23 at 05:03
  • Hi, @Wo_0NDeRᵀᴹ . I tried solutions from this link already and have the following problem. In my case my textField changes only x, width. If I scale it only by x, then text will be ugly shrinked. I need to find a way for a text to be able to fit it's aspect ratio while animating – artexhibit Mar 20 '23 at 08:27

2 Answers2

2

Try this https://i.stack.imgur.com/iHozb.gif

UIView.animate(withDuration: 1.0) {
    self.myTextField.transform = CGAffineTransform(scaleX: 2.0, y: 2.0)
}
DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Hi Kazi. Thank you for the reply. I tried that solution already and have the following problem. We scale textField by x and y equally. In my case my textField changes only x, width. If I scale it only by x, then text will be ugly shrinked. I need to find a way for a text to be able to fit it's aspect ratio while animating – artexhibit Mar 20 '23 at 08:26
1

I think you'll run into a number of different issues trying to change the frame of the text field.

One approach...

  • use a plain UIView as the "text field frame"
  • overlay a clear-background UITextField, sized to "max available width"

When "shrinking" the width...

  • animate the contraint constant change on the "frame" view
  • get the width of the text
  • calculate the needed scale value for that width
  • animate a .scaledBy transform on the text field

Here's an example cell (code-only, no @IBOutlet or @IBAction connections):

class ScaleTestCell:  UITableViewCell, UITextFieldDelegate {

    static let reuseIdentifier: String = "scaleTestCell"
    
    let blueLabel: UILabel = {
        let v = UILabel()
        // light blue
        v.backgroundColor = .init(red: 0.60, green: 0.80, blue: 1.00, alpha: 1.0)
        v.font = .systemFont(ofSize: 15, weight: .bold)
        return v
    }()
    let textField: UITextField = {
        let v = UITextField()
        v.backgroundColor = .clear
        v.font = .systemFont(ofSize: 30, weight: .bold)
        v.adjustsFontSizeToFitWidth = true
        v.minimumFontSize = 10.0
        v.textAlignment = .right
        v.keyboardType = .numberPad
        return v
    }()
    let sizingField: UITextField = {
        let v = UITextField()
        return v
    }()
    let tfFrameView: UIView = {
        let v = UIView()
        // medium green
        v.backgroundColor = .init(red: 0.60, green: 0.90, blue: 0.60, alpha: 1.0)
        return v
    }()
    
    let fullWidth: CGFloat = 300.0
    let compactWidth: CGFloat = 100.0
    var tfFrameWidth: NSLayoutConstraint!
    
    var tap: UITapGestureRecognizer!

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        
        // match font property of the "sizing" text field
        sizingField.font = textField.font

        // we will NOT use auto-layout for the textField
        [blueLabel, tfFrameView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        [blueLabel, tfFrameView, textField].forEach { v in
            contentView.addSubview(v)
        }
        
        let g = contentView
        
        // avoid auto-layout complaints
        let hc = blueLabel.heightAnchor.constraint(equalToConstant: 46.0)
        hc.priority = .required - 1

        tfFrameWidth = tfFrameView.widthAnchor.constraint(equalToConstant: compactWidth)
        
        NSLayoutConstraint.activate([
            
            blueLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            blueLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            blueLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            blueLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            
            hc,

            tfFrameView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            tfFrameView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            tfFrameView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            tfFrameWidth,

        ])

        textField.delegate = self
        
        // default text
        blueLabel.text = "Label"
        textField.text = "0"
        
        // add a tap gesture since the text field will be short when scaled
        tap = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
        tfFrameView.addGestureRecognizer(tap)
        
    }

    // only want to update the framing if the cell bounds width has changed
    var currentBoundsWidth: CGFloat = 0
    
    override func layoutSubviews() {
        super.layoutSubviews()

        if currentBoundsWidth != bounds.width {
            currentBoundsWidth = bounds.width

            var r: CGRect = .zero

            // use contentView.bounds    
            //r.size.height = bounds.height
            r.size.height = contentView.bounds.height

            // we're using 8-points "padding" on each side of the text field
            r.size.width = fullWidth - 16.0

            // use contentView.bounds    
            //r.origin.x = bounds.width - (r.size.width + 8.0)
            r.origin.x = contentView.bounds.width - (r.size.width + 8.0)

            // when scaling, we want to scale from the right-edge
            self.textField.layer.anchorPoint = .init(x: 1.0, y: 0.5)
            
            self.textField.frame = r

            let sfw: CGFloat = getWidth(ofString: textField.text ?? "")
            
            // we're using 8-points "padding" on each side of the text field
            let maxW: CGFloat = compactWidth - 16.0
            
            // calculate needed scale for the transform
            var scB: CGFloat = 1.0
            if sfw > maxW {
                scB = maxW / sfw
            }
            
            let tr: CGAffineTransform = .identity
            self.textField.transform = tr.scaledBy(x: scB, y: scB)
        }
    }

    func getWidth(ofString: String) -> CGFloat {
        sizingField.text = textField.text
        sizingField.sizeToFit()
        return sizingField.frame.width
    }
    
    @objc func gotTap(_ g: UITapGestureRecognizer) {
        
        // enable the textField
        textField.isUserInteractionEnabled = true
        // "activate" the textField
        textField.becomeFirstResponder()
        
    }
    
    func textFieldDidBeginEditing(_ textField: UITextField) {
        
        // remove the tap gesture
        tfFrameView.removeGestureRecognizer(tap)
        
        // animate the width change of the green "container" view
        //  and the scale transform of the textField
        tfFrameWidth.constant = fullWidth
        UIView.animate(withDuration: 1.0) {
            self.textField.transform = .identity
            self.layoutIfNeeded()
        }
        
    }

    func textFieldDidEndEditing(_ textField: UITextField) {

        let sfw: CGFloat = getWidth(ofString: textField.text ?? "")
        
        // we're using 8-points "padding" on each side of the text field
        let maxW: CGFloat = compactWidth - 16.0
        
        // calculate needed scale for the transform
        var scB: CGFloat = 1.0
        if sfw > maxW {
            scB = maxW / sfw
        }

        // animate the width change of the green "container" view
        //  and the scale transform of the textField
        tfFrameWidth.constant = compactWidth
        UIView.animate(withDuration: 1.0) {
            self.textField.transform = CGAffineTransform(scaleX: scB, y: scB)
            self.layoutIfNeeded()
        }
        
        // if the string is short, the "clear background" textField
        //  will extend outside the green "container" view, and we don't want
        //  tapping in the blue frame to activate the textField
        textField.isUserInteractionEnabled = false
        
        // re-add the tap gesture
        tfFrameView.addGestureRecognizer(tap)
    }

}

and your controller - modified so we have 4 rows with your cell, and 4 rows of the example ScaleTestCell (with varying initial text):

class TableViewController: UITableViewController {

    var myData: [String] = [
        "123",
        "123456",
        "1234567890",
        "230000034343",
        "230000034343",
        "1234567890",
        "123456",
        "123",
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        setupKeyboardBehaviour()

        // register non-Storyboard-Prototype cell
        tableView.register(ScaleTestCell.self, forCellReuseIdentifier: ScaleTestCell.reuseIdentifier)
        
    }

    // MARK: - Table view data source

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

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        // if it's one of the first half of the rows
        if indexPath.row < myData.count / 2 {
            let cell = tableView.dequeueReusableCell(withIdentifier: "testCell", for: indexPath) as! TestTableViewCell
            cell.textField.text = myData[indexPath.row]
            return cell
        }
        
        let cell = tableView.dequeueReusableCell(withIdentifier: ScaleTestCell.reuseIdentifier, for: indexPath) as! ScaleTestCell
        cell.textField.text = myData[indexPath.row]
        return cell
        
    }
    
    private func setupKeyboardBehaviour() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(viewTapped))
        view.addGestureRecognizer(tap)
    }
    
    @objc func viewTapped(sender: UITapGestureRecognizer) {
        view.endEditing(true)
    }
}

Looks about like this:

enter image description here

Note: SAMPLE CODE ONLY!!! -- Needs complete testing... If you're going to allow table view frame changing (such as on device rotation), you've got a little more work to do.


Edit

I should have caught this to begin with... To get proper behavior when the cell size changes - such as on device rotation...

In layoutSubviews() we want to set the frame of the text field relative to the contentView.bounds, not to the cell's bounds.

I updated the above cell class code with that change (two lines affected).

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • Hi, DonMag. Thank you for such a detailed answer! This is exactly what I am looking for. Yes, I allow device rotation, but will dig into your code and try to sort it out – artexhibit Mar 21 '23 at 05:20
  • 1
    @artexhibit - I took another quick look... just needed to change two lines in `layoutSubviews()` ... see the **Edit** – DonMag Mar 21 '23 at 11:40
  • thank you very much! Learnt a lot from your answers – artexhibit Mar 21 '23 at 11:57