Floating UILabel on UITextField with custom border
Asked Answered
R

4

6

I'm basically trying to create a UITextField which, when the user has typed something, shows the placeholder text in a UILabel pretty much on the top border.

I've managed to get the UILabel into position with animations and everything, except the UITextField's border is running through the UILabel when I give it (the border) a custom colour and width. If I leave the standard RoundedRect border, without giving borderColor or borderWidth, it all works perfectly. I need the colour though :/

(Ignore the red background - doing it for visibility)

Custom borderColor goes through label Custom borderColor goes through label

And this is with no custom border (see - no border through the UILabel 👏)

Standard roundedRect border

So yeah - any help would be appreciated. Thanks 😀🙏

Richmound answered 7/8, 2019 at 7:22 Comment(2)
I have actually not tested this, but have you tried changing the layer z position?Dampier
@Dampier yeah I have.. Unfortunately frame gets rendered on top of the object it's attached to - above all the subviews regardless. So zIndex doesn't make a difference.Richmound
R
0

Thanks everyone for the answers 🙏 Been sitting with this for ages and 2 hours after I resort to posting here, I figure it out. Typical.

I was specifically trying to make a subclass of UITextField so I can reuse it all over the place without much hassle. That's why I didn't want to make a UIView which contained UITextField and UILabel as subviews - I'd then have to do that every time... Not ideal.

So after digging into the view hierarchy of a UITextField, it turns out there is a subview _UITextFieldRoundedRectBackgroundViewNeue which is always first and it is the one which holds the roundedRect border. Adding a custom border via layer.border actually adds it on the frame - on top of the entire view hierarchy. That's why it would rule infront of the UILabel.

So what I did was apply a layer.border to that _UITextFieldRoundedRectBackgroundViewNeue, essentially forcing the border to always be at the bottom, behind everything.

I'd suggest anyone doing this do a check if subviews[0].classForCoder.description() == "_UITextFieldRoundedRectBackgroundViewNeue" just to make sure it's actually there and to make sure you're not adding a border to a random subview you don't expect. 😊

Now, adding a UILabel to the actual UITextField object, with a white background, will give the effect of a floating label. Sweet. 🥳

enter image description here

Richmound answered 7/8, 2019 at 9:31 Comment(4)
Not sure how safe is to have the implementation by checking that specific description, It might be that in future releases of iOS they change the inner specification for it, and the code might fail to achieve the desired results in the UI. Till then, I am happy you found a new way to solve it :)Spoonbill
Yeah I agree - thought of that too. I tried to do a check against a class type (since that is less likely to change) but couldn't actually find a class type for it :/ Will try a few other variations but overall, I agree with you :)Richmound
Hey @ByronCoetsee Can you tell me how you have implemented this whole thing? I am trying to do the same, but not getting any idea.Paramatta
can you please share demo projects?Petta
S
3

From Apple's documentation in CALayer:

The border is drawn inset from the receiver’s bounds by borderWidth. It is composited above the receiver’s contents and sublayers and includes the effects of the cornerRadius property.

But you can achieve your results by putting the text into an own subview/sublayer.

As an example create a common superview for your UITextField and placeholderlabel (don't add placeholderlabel as a subview of UITextField, add them both to the new superview UIView for example).

Spoonbill answered 7/8, 2019 at 7:41 Comment(2)
Thanks Denis, but I'm specifically trying to create a subclass of UITextField for reuse elsewhere... So my answer - I figured it out :) Thanks for the help though!Richmound
I'm sorry you couldn't achieve it that way, you could reuse the parent UIView in your code if setup correctly perhaps? Anyway glad you solved it in other ways!Spoonbill
U
1

This didn't help for me. I didn't find "_UITextFieldRoundedRectBackgroundViewNeue". But that help me: in layoutsubviews() I create two layers and after that add floating placeholder Label in updateLayer(). Floating Label is placed above whiteLabel

public var floatingLabel = UILabel()
private var borderLayer = CAShapeLayer()
private var whiteLayer: CAShapeLayer = {
    let layer = CAShapeLayer()
    let path = UIBezierPath(rect: CGRect(x: 8, y: -2, width: 50, height: 4))
    layer.fillColor = UIColor.white.cgColor
    layer.path = path.cgPath
    return layer
}()

private func updateLayer() {
    layer.insertSublayer(borderLayer, at: 999)
    layer.insertSublayer(whiteLayer, above: borderLayer)
    let path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 4)
    borderLayer.path = path.cgPath
    borderLayer.lineWidth = 1
    borderLayer.fillColor = UIColor.clear.cgColor
    addSubview(floatingLabel)
}

then hide or show placeHolderLabel just hide whiteLayer and then you need - add borderLayer.strokeColor

Unbreathed answered 12/8, 2020 at 13:24 Comment(0)
G
1

Neither the two answers worked for me on ios 15 and swift 5.

I had to extend the textfield i didnt want a new view

class PrimaryTextField:UITextField{

then i override the draw function adding a border layer

override func draw(_ rect: CGRect) {
        
        addBorder(borderWidth: borderWidth, borderColor: blue5Color)

...

func addBorder(borderWidth: CGFloat = 1.0, borderColor: UIColor = UIColor.blue) {
        
        let frame = self.frame
        borderLayer.frame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)
        borderLayer.borderColor = borderColor.cgColor
        borderLayer.borderWidth = borderWidth
        borderLayer.zPosition = 1
        self.layer.addSublayer(borderLayer)
    }

this will control it with the zPosition the layer to be on position 1

And the label i added with zPosition 2 adding it as a subview

func customInit(){
self.lblPlaceHolder.layer.zPosition = 2
self.addSubview(self.lblPlaceHolder)

...

I added the label on the custom init method overrided here

required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        self.customInit()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.customInit()
    }

With this the label appears on front of the border.

Griffe answered 2/9, 2022 at 15:53 Comment(0)
R
0

Thanks everyone for the answers 🙏 Been sitting with this for ages and 2 hours after I resort to posting here, I figure it out. Typical.

I was specifically trying to make a subclass of UITextField so I can reuse it all over the place without much hassle. That's why I didn't want to make a UIView which contained UITextField and UILabel as subviews - I'd then have to do that every time... Not ideal.

So after digging into the view hierarchy of a UITextField, it turns out there is a subview _UITextFieldRoundedRectBackgroundViewNeue which is always first and it is the one which holds the roundedRect border. Adding a custom border via layer.border actually adds it on the frame - on top of the entire view hierarchy. That's why it would rule infront of the UILabel.

So what I did was apply a layer.border to that _UITextFieldRoundedRectBackgroundViewNeue, essentially forcing the border to always be at the bottom, behind everything.

I'd suggest anyone doing this do a check if subviews[0].classForCoder.description() == "_UITextFieldRoundedRectBackgroundViewNeue" just to make sure it's actually there and to make sure you're not adding a border to a random subview you don't expect. 😊

Now, adding a UILabel to the actual UITextField object, with a white background, will give the effect of a floating label. Sweet. 🥳

enter image description here

Richmound answered 7/8, 2019 at 9:31 Comment(4)
Not sure how safe is to have the implementation by checking that specific description, It might be that in future releases of iOS they change the inner specification for it, and the code might fail to achieve the desired results in the UI. Till then, I am happy you found a new way to solve it :)Spoonbill
Yeah I agree - thought of that too. I tried to do a check against a class type (since that is less likely to change) but couldn't actually find a class type for it :/ Will try a few other variations but overall, I agree with you :)Richmound
Hey @ByronCoetsee Can you tell me how you have implemented this whole thing? I am trying to do the same, but not getting any idea.Paramatta
can you please share demo projects?Petta

© 2022 - 2024 — McMap. All rights reserved.