6

I'm trying to add animation to the border of UITextField when edited by the user.

The idea is to show line animation in the login page once the first text field is being edited and then after the user switches to the next textfield, the line should make a movement to the below text field.

I have made an attempt which is not working as what I'm expecting.

My code:

class ViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet weak var verticSpace: NSLayoutConstraint!
    @IBOutlet weak var usernameTxtField: UITextField!
    @IBOutlet weak var passwordTxtField: UITextField!

    weak var shapeLayer: CAShapeLayer?


    let path = UIBezierPath()

    let shapeLayerNew = CAShapeLayer()

    override func viewDidLoad() {
        super.viewDidLoad()

        usernameTxtField.delegate = self

        passwordTxtField.delegate = self

    }


    func textFieldDidBeginEditing(_ textField: UITextField) {
        let path = UIBezierPath()

        if textField == usernameTxtField {
            if textField.text == "" {

                self.startMyLine()
            }

        }

        if passwordTxtField.isFirstResponder {

            let path2 = UIBezierPath()


            path2.move(to: CGPoint(x: usernameTxtField.frame.width, y: usernameTxtField.frame.height - shapeLayerNew.lineWidth))
            path2.addLine(to: CGPoint(x: usernameTxtField.frame.width, y: (usernameTxtField.frame.height - shapeLayerNew.lineWidth) + passwordTxtField.frame.height + verticSpace.constant))
            path2.addLine(to: CGPoint(x: 0, y: (usernameTxtField.frame.height - shapeLayerNew.lineWidth) + passwordTxtField.frame.height + verticSpace.constant))

            let combinedPath = path.cgPath.mutableCopy()
            combinedPath?.addPath(path2.cgPath)
            shapeLayerNew.path = path2.cgPath

            let startAnimation = CABasicAnimation(keyPath: "strokeStart")
            startAnimation.fromValue = 0
            startAnimation.toValue = 0.8

            let endAnimation = CABasicAnimation(keyPath: "strokeEnd")
            endAnimation.fromValue = 0.2
            endAnimation.toValue = 1.0

            let animation = CAAnimationGroup()
            animation.animations = [startAnimation, endAnimation]
            animation.duration = 2
            shapeLayerNew.add(animation, forKey: "MyAnimation")

        }

    }

    func startMyLine() {

        self.shapeLayer?.removeFromSuperlayer()

        // create whatever path you want


        shapeLayerNew.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
        shapeLayerNew.strokeColor = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1).cgColor
        shapeLayerNew.lineWidth = 4


        path.move(to: CGPoint(x: 0, y: usernameTxtField.frame.height - shapeLayerNew.lineWidth))
        path.addLine(to: CGPoint(x: usernameTxtField.frame.width, y: usernameTxtField.frame.height - shapeLayerNew.lineWidth))


        // create shape layer for that path


        shapeLayerNew.path = path.cgPath

        // animate it

        usernameTxtField.layer.addSublayer(shapeLayerNew)

        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = 0
        animation.duration = 2
        shapeLayerNew.add(animation, forKey: "MyAnimation")

        // save shape layer

        self.shapeLayer = shapeLayerNew


    }
}

My result:

My attempt

Expected result:

What I need to see

Edit 1:

I have applied the changes based on @SWAT answer, but I still cannot get the expected result. When the username's field is being edited, I get the quad line displayed while it should only appear when moving to the next text field, and then the quad line should disappear after the animation is finished.

My updated code:

class ViewController: UIViewController, UITextFieldDelegate {

@IBOutlet weak var usernameTxtField: UITextField!
@IBOutlet weak var passwordTxtField: UITextField!

weak var shapeLayer: CAShapeLayer?


let path = UIBezierPath()

let shapeLayerNew = CAShapeLayer()

var animLayer = CAShapeLayer()

override func viewDidLoad() {
    super.viewDidLoad()

    usernameTxtField.delegate = self

    passwordTxtField.delegate = self


}



func textFieldDidBeginEditing(_ textField: UITextField) {

    if textField == usernameTxtField{

        var path = UIBezierPath()

        path.move(to: CGPoint.init(x: self.usernameTxtField.frame.minX, y: self.usernameTxtField.frame.maxY))
        path.addLine(to: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))
        path.addQuadCurve(to: CGPoint.init(x: self.passwordTxtField.frame.maxX, y: self.passwordTxtField.frame.maxY), controlPoint: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))
        path.addLine(to: CGPoint.init(x: self.passwordTxtField.frame.minX, y: self.passwordTxtField.frame.maxY))



        animLayer.fillColor = UIColor.clear.cgColor
        animLayer.path = path.cgPath
        animLayer.strokeColor = UIColor.cyan.cgColor
        animLayer.lineWidth = 3.0
        self.view.layer.addSublayer(animLayer)

        animLayer.strokeEnd = 0
        animLayer.strokeStart = 0


        let initialAnimation                   = CABasicAnimation(keyPath: "strokeEnd")
        initialAnimation.toValue               = 0.5
        initialAnimation.beginTime             = 0
        initialAnimation.duration              = 0.5
        initialAnimation.fillMode              = kCAFillModeBoth
        initialAnimation.isRemovedOnCompletion = false

        animLayer.add(initialAnimation, forKey: "usernameFieldStrokeEnd")

        let secondTextFieldAnimStrokeStart                  = CABasicAnimation(keyPath: "strokeStart")
        secondTextFieldAnimStrokeStart.toValue               = 0
        secondTextFieldAnimStrokeStart.beginTime             = 0
        secondTextFieldAnimStrokeStart.duration              = 0.5
        secondTextFieldAnimStrokeStart.fillMode              = kCAFillModeBoth
        secondTextFieldAnimStrokeStart.isRemovedOnCompletion = false

        animLayer.add(secondTextFieldAnimStrokeStart, forKey: "usernameFieldStrokeStart")


    } else {
        var path = UIBezierPath()

        path.move(to: CGPoint.init(x: self.usernameTxtField.frame.minX, y: self.usernameTxtField.frame.maxY))
        path.addLine(to: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))


        path.addQuadCurve(to: CGPoint.init(x: self.passwordTxtField.frame.maxX, y: self.passwordTxtField.frame.maxY), controlPoint: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))
        path.addLine(to: CGPoint.init(x: self.passwordTxtField.frame.minX, y: self.passwordTxtField.frame.maxY))



        animLayer.fillColor = UIColor.clear.cgColor
        animLayer.path = path.cgPath
        animLayer.strokeColor = UIColor.cyan.cgColor
        animLayer.lineWidth = 3.0
        self.view.layer.addSublayer(animLayer)

        animLayer.strokeEnd = 0
        animLayer.strokeStart = 0

        let secondTextFieldAnimStrokeEnd                   = CABasicAnimation(keyPath: "strokeEnd")
        secondTextFieldAnimStrokeEnd.toValue               = 1.0
        secondTextFieldAnimStrokeEnd.beginTime             = 0
        secondTextFieldAnimStrokeEnd.duration              = 0.5
        secondTextFieldAnimStrokeEnd.fillMode              = kCAFillModeBoth
        secondTextFieldAnimStrokeEnd.isRemovedOnCompletion = false

        animLayer.add(secondTextFieldAnimStrokeEnd, forKey: "secondTextFieldStrokeEnd")

        let secondTextFieldAnimStrokeStart                  = CABasicAnimation(keyPath: "strokeStart")
        secondTextFieldAnimStrokeStart.toValue               = 0.5
        secondTextFieldAnimStrokeStart.beginTime             = 0
        secondTextFieldAnimStrokeStart.duration              = 0.5
        secondTextFieldAnimStrokeStart.fillMode              = kCAFillModeBoth
        secondTextFieldAnimStrokeStart.isRemovedOnCompletion = false

        animLayer.add(secondTextFieldAnimStrokeStart, forKey: "secondTextFieldStrokeStart")
    }
}

}

This is what I'm getting now:

Updated Result

Edit 2:

I managed to find a way that gave me a fairly close result to what I'm expecting. I have set isRemoveCompletion to true in order to erase the line when the animation is finished, and then add a bottom border to the textfield.

class ViewController: UIViewController, UITextFieldDelegate {

@IBOutlet weak var usernameTxtField: UITextField!
@IBOutlet weak var passwordTxtField: UITextField!


var animLayer = CAShapeLayer()

let newLayer2 = CAShapeLayer()

override func viewDidLoad() {
    super.viewDidLoad()

    usernameTxtField.delegate = self

    passwordTxtField.delegate = self


    makePath()

}

func makePath(){
    //var path = UIBezierPath()

    let path = UIBezierPath()

    path.move(to: CGPoint.init(x: self.usernameTxtField.frame.minX, y: self.usernameTxtField.frame.maxY))
    path.addLine(to: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))
    path.addQuadCurve(to: CGPoint.init(x: self.passwordTxtField.frame.maxX, y: self.passwordTxtField.frame.maxY), controlPoint: CGPoint.init(x: self.usernameTxtField.frame.maxX, y: self.usernameTxtField.frame.maxY))
    path.addLine(to: CGPoint.init(x: self.passwordTxtField.frame.minX, y: self.passwordTxtField.frame.maxY))


    animLayer.fillColor = UIColor.clear.cgColor
    animLayer.path = path.cgPath
    animLayer.strokeColor = UIColor(red: 214/255, green: 54/255, blue: 57/255, alpha: 1).cgColor
    animLayer.lineWidth = 3.0
    animLayer.lineCap = kCALineCapRound
    animLayer.lineJoin = kCALineJoinRound
    self.view.layer.addSublayer(animLayer)

    animLayer.strokeEnd = 0
    animLayer.strokeStart = 0
}

func addBottomBorder(textField: UITextField) {
    var path = UIBezierPath()

    path.move(to: CGPoint.init(x: textField.frame.minX, y: textField.frame.maxY))
    path.addLine(to: CGPoint.init(x: textField.frame.maxX, y: textField.frame.maxY))

    self.newLayer2.fillColor = UIColor.clear.cgColor
    self.newLayer2.path = path.cgPath
    self.newLayer2.strokeColor = UIColor(red: 214/255, green: 54/255, blue: 57/255, alpha: 1).cgColor
    self.newLayer2.lineWidth = 3.0
    self.newLayer2.lineCap = kCALineCapRound
    self.newLayer2.lineJoin = kCALineJoinRound
    self.view.layer.addSublayer(self.newLayer2)
}

func textFieldDidBeginEditing(_ textField: UITextField) {
    if textField == usernameTxtField{
        CATransaction.begin()
        self.newLayer2.removeFromSuperlayer()
        let initialAnimation                   = CABasicAnimation(keyPath: "strokeEnd")
        initialAnimation.toValue               = 0.45
        initialAnimation.beginTime             = 0
        initialAnimation.duration              = 1.0
        initialAnimation.fillMode              = kCAFillModeBoth
        initialAnimation.isRemovedOnCompletion = true
        initialAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)

        animLayer.add(initialAnimation, forKey: "usernameFieldStrokeEnd")

        let secondTextFieldAnimStrokeStart                  = CABasicAnimation(keyPath: "strokeStart")
        secondTextFieldAnimStrokeStart.toValue               = 0
        secondTextFieldAnimStrokeStart.beginTime             = 0
        secondTextFieldAnimStrokeStart.duration              = 1.0
        secondTextFieldAnimStrokeStart.fillMode              = kCAFillModeBoth
        secondTextFieldAnimStrokeStart.isRemovedOnCompletion = true
        secondTextFieldAnimStrokeStart.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)

        CATransaction.setCompletionBlock {
            if !self.passwordTxtField.isFirstResponder {
                self.addBottomBorder(textField: self.usernameTxtField)
            }
        }

        animLayer.add(secondTextFieldAnimStrokeStart, forKey: "usernameFieldStrokeStart")

        CATransaction.commit()


    } else {
        CATransaction.begin()
        self.newLayer2.removeFromSuperlayer()

        let secondTextFieldAnimStrokeEnd                   = CABasicAnimation(keyPath: "strokeEnd")
        secondTextFieldAnimStrokeEnd.toValue               = 1.0
        secondTextFieldAnimStrokeEnd.beginTime             = 0
        secondTextFieldAnimStrokeEnd.duration              = 1.0
        secondTextFieldAnimStrokeEnd.fillMode              = kCAFillModeBoth
        secondTextFieldAnimStrokeEnd.isRemovedOnCompletion = true
        secondTextFieldAnimStrokeEnd.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)

        animLayer.add(secondTextFieldAnimStrokeEnd, forKey: "secondTextFieldStrokeEnd")

        let secondTextFieldAnimStrokeStart                  = CABasicAnimation(keyPath: "strokeStart")
        secondTextFieldAnimStrokeStart.toValue               = 0.5
        secondTextFieldAnimStrokeStart.beginTime             = 0
        secondTextFieldAnimStrokeStart.duration              = 1.0
        secondTextFieldAnimStrokeStart.fillMode              = kCAFillModeBoth
        secondTextFieldAnimStrokeStart.isRemovedOnCompletion = true
        secondTextFieldAnimStrokeStart.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)

        CATransaction.setCompletionBlock {
            if !self.usernameTxtField.isFirstResponder {
                self.addBottomBorder(textField: self.passwordTxtField)
            }
        }


        animLayer.add(secondTextFieldAnimStrokeStart, forKey: "secondTextFieldStrokeStart")

        CATransaction.commit()
    }

}

enter image description here

Maryoomi1
  • 113
  • 1
  • 3
  • 16

2 Answers2

2

Your desired animation is very cool. It's going to be a lot of work to implement though. I've done quite bit of Core Animation and creating your whole animation sequence would probably take me a couple of days.

The basic rule with Core animation path animation is that the starting path and ending path have to have the same number and type of control points. You're going to need to divide your animation into multiple segments and animate them separately.

For some parts (where the shape doesn't change but you add/remove pixels to the path like you're drawing with a pen and/or erasing parts you've previously drawn) you'll have a fixed path and animate the strokeStart and strokeEnd properties.

For other sections of the animation (where the shape changes) you'll have to carefully construct starting and ending paths that have the same number and type of control points and the desired starting and ending shapes and animate between them. (This might mean that for some animations you create a starting or ending path that has a lot of sub-paths that actually draw as much simpler shapes.) It would take quite bit of thought to figure out how to do that.

The first step will be to diagram your animation and break it into stages.

Duncan C
  • 128,072
  • 22
  • 173
  • 272
2

This is not a simple thing to be explained as a StackOverflow answer.

But still, I will give you an idea of how to implement it.

You should first make a BezierPath:

func makePath(){
    var path = UIBezierPath()
    path.move(to: CGPoint.init(x: self.usernameField.frame.minX, y: self.usernameField.frame.maxY))
    path.addLine(to: CGPoint.init(x: self.usernameField.frame.maxX, y: self.usernameField.frame.maxY))
    path.addQuadCurve(to: CGPoint.init(x: self.passwordField.frame.maxX, y: self.passwordField.frame.maxY), controlPoint: CGPoint.init(x: self.usernameField.frame.maxX + 10, y: self.usernameField.frame.maxY + 10))
    path.addLine(to: CGPoint.init(x: self.passwordField.frame.minX, y: self.passwordField.frame.maxY))



    animLayer.fillColor = UIColor.clear.cgColor
    animLayer.path = path.cgPath
    animLayer.strokeColor = UIColor.cyan.cgColor
    animLayer.lineWidth = 3.0
    self.view.layer.addSublayer(animLayer)

    animLayer.strokeEnd = 0
    animLayer.strokeStart = 0
}

Add the animations in the TextFieldDelegate overrides:

extension CustomLoginAnimmationController: UITextFieldDelegate{
func textFieldDidBeginEditing(_ textField: UITextField) {

// All the 0.5 for strokeEnd and strokeStart means 50%, You will have to calculate yourself, what percentage value you must add here
    if textField == usernameField{
        let initialAnimation                   = CABasicAnimation(keyPath: "strokeEnd")
        initialAnimation.toValue               = 0.5
        initialAnimation.beginTime             = 0
        initialAnimation.duration              = 0.5
        initialAnimation.fillMode              = kCAFillModeBoth
        initialAnimation.isRemovedOnCompletion = false

        animLayer.add(initialAnimation, forKey: "usernameFieldStrokeEnd")

        let secondTextFieldAnimStrokeStart                  = CABasicAnimation(keyPath: "strokeStart")
        secondTextFieldAnimStrokeStart.toValue               = 0
        secondTextFieldAnimStrokeStart.beginTime             = 0
        secondTextFieldAnimStrokeStart.duration              = 0.5
        secondTextFieldAnimStrokeStart.fillMode              = kCAFillModeBoth
        secondTextFieldAnimStrokeStart.isRemovedOnCompletion = false

        animLayer.add(secondTextFieldAnimStrokeStart, forKey: "usernameFieldStrokeStart")
    } else {
        let secondTextFieldAnimStrokeEnd                   = CABasicAnimation(keyPath: "strokeEnd")
        secondTextFieldAnimStrokeEnd.toValue               = 1.0
        secondTextFieldAnimStrokeEnd.beginTime             = 0
        secondTextFieldAnimStrokeEnd.duration              = 0.5
        secondTextFieldAnimStrokeEnd.fillMode              = kCAFillModeBoth
        secondTextFieldAnimStrokeEnd.isRemovedOnCompletion = false

        animLayer.add(secondTextFieldAnimStrokeEnd, forKey: "secondTextFieldStrokeEnd")

        let secondTextFieldAnimStrokeStart                  = CABasicAnimation(keyPath: "strokeStart")
        secondTextFieldAnimStrokeStart.toValue               = 0.5 
        secondTextFieldAnimStrokeStart.beginTime             = 0
        secondTextFieldAnimStrokeStart.duration              = 0.5
        secondTextFieldAnimStrokeStart.fillMode              = kCAFillModeBoth
        secondTextFieldAnimStrokeStart.isRemovedOnCompletion = false

        animLayer.add(secondTextFieldAnimStrokeStart, forKey: "secondTextFieldStrokeStart")
    }

}
SWAT
  • 1,107
  • 8
  • 19
  • Thank you @SWAT for the answer. I tried your solution but I'm still experiencing issues. I have edited my question, please check it. – Maryoomi1 May 20 '18 at 10:01
  • That is exactly the output of my code. What I'm trying to tell with my answer here, is a way in which you can reach your solution. You must dynamically calculate the percentage value that I have applied there (50% in my answer) to whatever you would want in your case. – SWAT May 20 '18 at 10:14
  • And about the line fully disappearing and other things you mentioned can be made using adjustments to my code, as per your project's requirement. – SWAT May 20 '18 at 10:17
  • Yes @SWAT I have tried calculating the percentage of animation value, but the calculation keeps changing when the screen size is different. Do you have any idea about how to stop the stroke animation after detecting that it has reached the end of first textfield's bounds? And then remove the quad curve after detecting that the animation of second textfield's has finished? – Maryoomi1 May 20 '18 at 17:57
  • Hi @SWAT I have found a way to solve the issue. Please check the second edit on my question. – Maryoomi1 May 23 '18 at 10:00