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:

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