Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITextField, text jumps down slightly when editing begins

Tags:

I'm having an odd issue here. I have UITextFields in my table cells. When I select the field the text jumps very slightly down:

enter image description here

The font is system default 17. I have adjust to fit turned on at size 17. I have tried turning off adjust to fit and there is still a jump. I have tried using different border styles and this also makes no difference. I have tried turning off clip to bounds, it still jumps. I have also tried making the frame taller (much taller) and it still jumps. The only thing that works is if I make the font size much smaller eg 13. What am I doing wrong here? If I can make the font smaller to fix the jump then why doesn't making the frame bigger work? Any pointers on this would be really appreciated. Thanks!

like image 802
KexAri Avatar asked Mar 29 '17 05:03

KexAri


1 Answers

I must admit that I've always seen this little bouncing during my developments but I never investigate around it. Some points that I've used to replicate your issue:

  1. two textfields with default dimensions and settings
  2. using system default font and change it's size from 13 to 25 just to test the behavior

I suppose there are more other ways to solve your problem but I don't find a fast property to stop this little "jumping" of text. I've decide to analize the current UITextField ($ xcrun swift -version = Swift 3.1) to see it's composition:

import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
    @IBOutlet weak var myTextField: UITextField!
    override func viewDidLoad() {
        super.viewDidLoad()
        myTextField.delegate = self
    }
    func textFieldDidBeginEditing(_ textField: UITextField) {
        print("\ntextFieldDidBeginEditing")
        showLayersDescription()
    }
    func textFieldDidEndEditing(_ textField: UITextField) {
        print("\ntextFieldDidEndEditing")
        showLayersDescription()
    }
    func showLayersDescription() {
        myTextField.layer.sublayers?.forEach{ print($0.debugDescription)}
    }
}

where myTextField is essentially one of the two textfields

Output using font size 17:

enter image description here

Essentially seems there are 3 CALayer:

  • a layer with frame = CGRect (0 0; 252 30) that have _UITextFieldRoundedRectBackgroundViewNeue as delegate that have the same dimension of our textfield
  • a layer with frame = CGRect (7 2; 238 26) that is showed only in our textFieldDidBeginEditing and it have UIFieldEditor as delegate that seems to be the responsible for the editing part..
  • a layer that appear only in textFieldDidEndEditing with frame = CGRect.zero and with delegate UITextFieldLabel

If I write something to the first textField then I go to the next textfield nothing happened BUT if I return to the first textField and then to the second the layer with frame equal to CGRect.zero change to frame = CGRect(7 0.5; 238 27.5)

This is the little height (0.5) that we can see during the beginning and the ending of our editing.

We can try to refine the debug and intercept these layers with an extension:

extension UITextField
{
    func debugLayers() {
        print("We have the follow layers:")
        let FieldEditor: AnyObject.Type = NSClassFromString("UIFieldEditor")!
        let LabelLayer: AnyObject.Type = NSClassFromString("_UILabelLayer")!
        let TextFieldRoundRect: AnyObject.Type = NSClassFromString("_UITextFieldRoundedRectBackgroundViewNeue")!
        self.layer.sublayers?.forEach{
            if ($0.delegate?.self.isKind(of: TextFieldRoundRect))! { print("- layer with _UITextFieldRoundedRectBackgroundViewNeue as delegate have frame:\($0.frame)") }
            if ($0.delegate?.self.isKind(of: FieldEditor))! { print("- layer with UIFieldEditor as delegate have frame:\($0.frame)") }
            if $0.self.isKind(of: LabelLayer) { print("- layer is kind of _UILabelLayer have frame:\($0.frame)") }
        }
    }
}

So we have for example:

import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
    @IBOutlet weak var myTextField: UITextField!
    override func viewDidLoad() {
        super.viewDidLoad()
        myTextField.delegate = self
    }
    func textFieldDidBeginEditing(_ textField: UITextField) {
        print("\ntextFieldDidBeginEditing")
        myTextField.debugLayers()
    }
    func textFieldDidEndEditing(_ textField: UITextField) {
        print("\ntextFieldDidEndEditing")
        myTextField.debugLayers()
    }
}

Output always with font size 17:

enter image description here

As we can see we have always this 0.5 difference in height in that layer..

Making other tries I've seen that this behaviour happened only if the default size is between 13 and 17, under 13 and from 18 to 25 this not happened has you've report to your question.

A solution:

I think the best way to intercept and trying to correct this one it's to make a new extension:

extension UITextField
{
    override open func layoutSubviews() {
        super.layoutSubviews()
        let FieldEditor: AnyObject.Type = NSClassFromString("UIFieldEditor")!
        let LabelLayer: AnyObject.Type = NSClassFromString("_UILabelLayer")!
        self.layer.sublayers?.forEach{
            if ($0.delegate?.self.isKind(of: FieldEditor))! {
                var f = $0.frame
                f.origin.y = 0.0
                $0.frame = f
            }
            if $0.self.isKind(of: LabelLayer) {
                var layerFrame = CGRect.zero
                layerFrame.origin = self.editingRect(forBounds: self.bounds).origin
                layerFrame.size = self.editingRect(forBounds: self.bounds).size
                if let size = self.font?.pointSize, 14 ... 17 ~= size {
                    layerFrame.origin.y = -0.5
                } else {
                    layerFrame.origin.y = 0.0
                }
                $0.frame = layerFrame
            }
        }
    }
} 

Final considerations:

This extension suppress the little "jumping down" of the text during the change to another textField. This is probably to balance the :

contentsCenter = CGRect (0.485 0.485; 0.000588235 0.000588235)

that both the second and third layer have with this group of font sizes. As you see in the extension I've set to zero also the height origin of the layer with UIFieldEditor delegate (that before have 2.0 as height) because it's involved to this change, I've maded it to balance the constraints differences.

Update for a valid and approved extension:

I've read your comment so I've analyzed in deep the situation about layers: what I've found is that the layer who have the gap of -0.5 height NEVER present sublayers, when the other ALWAYS present one sublayer ( have UIFieldEditor as delegate) so we can easily correct the extension as:

extension UITextField
{
    override open func layoutSubviews() {
        super.layoutSubviews()
        self.layer.sublayers?.forEach{
            if let subs = $0.sublayers, subs.count>0 {
                var f = $0.frame
                f.origin.y = 0.0
                $0.frame = f
            } else {
                var layerFrame = CGRect.zero
                layerFrame.origin = self.editingRect(forBounds: self.bounds).origin
                layerFrame.size = self.editingRect(forBounds: self.bounds).size
                if let size = self.font?.pointSize, 14 ... 17 ~= size {
                    layerFrame.origin.y = -0.5
                } else {
                    layerFrame.origin.y = 0.0
                }
                $0.frame = layerFrame
            }
        }
    }
}

I've tested this new extension and it behaves correctly like the old one.

like image 197
Alessandro Ornano Avatar answered Sep 30 '22 21:09

Alessandro Ornano