This is the 8th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

This series of articles focuses on the process of System Learning iOS Animation. This book is a great way to learn iOS animation, including view animation, automatic layout, layer animation, View Controller transitions, UIViewPropertyAnimator, 3D animation, and other types of animation.

Today we’re going to focus on UIKit animation apis, which are designed to make it easy to animate views. The UIKit animation API is not only easy to use, but also provides a great deal of flexibility and power to handle most animation requirements, including:

  • Position and size: Bounds, Frame, Center
  • Appearance: backgroundColor, alpha (to create fade in and out effects)
  • Transform: Sets the view’s rotation, scaling, and/or position to animate.

This is the effect to be achieved:

Start by adding these widgets:

import UIKit class ViewController: UIViewController { let screenWidth = UIScreen.main.bounds.size.width let screenHeight = UIScreen.main.bounds.size.height  let titleLabel = UILabel() let backgroundImage = UIImageView() let usernameTextField = UITextField() let passwordTextField = UITextField() let loginButton = UIButton() let cloud1 = UIImageView() let cloud2 = UIImageView() let  cloud3 = UIImageView() let cloud4 = UIImageView() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. view.addSubview(backgroundImage) view.addSubview(titleLabel) view.addSubview(usernameTextField) view.addSubview(passwordTextField) view.addSubview(loginButton) view.addSubview(cloud1) view.addSubview(cloud2) view.addSubview(cloud3) view.addSubview(cloud4) backgroundImage.image = UIImage(named: "bg-sunny") backgroundImage.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight) titleLabel.text = "Bahama Login" titleLabel.textColor = .white titleLabel.font = UIFont.systemFont(ofSize:  28) let titleWidth = titleLabel.intrinsicContentSize.width titleLabel.frame = CGRect(x: (screenWidth - titleWidth) / 2 , y: 120, width: titleWidth, height: titleLabel.intrinsicContentSize.height) let textFieldWidth = screenWidth - 60 usernameTextField.backgroundColor = .white  usernameTextField.layer.cornerRadius = 5 usernameTextField.placeholder = " Username" usernameTextField.frame = CGRect(x: 30, y: 202, width: textFieldWidth, height: 40) passwordTextField.backgroundColor = .white passwordTextField.layer.cornerRadius = 5 passwordTextField.placeholder = " Password" passwordTextField.frame = CGRect(x: 30, y: 263, width: textFieldWidth, height: 40) let buttonWidth = 260 loginButton.frame = CGRect(x: (Int(screenWidth) - buttonWidth) / 2, y: 343, width: buttonWidth, height: 50) loginButton.setTitle("Login", for: .normal) loginButton.setTitleColor(.red, for: .normal) loginButton.layer.cornerRadius = 5 loginButton.backgroundColor = .green cloud1.frame = CGRect(x: -120, y: 79, width: 160, height: 50) cloud1.image = UIImage(named: "bg-sunny-cloud-1") cloud2.frame = CGRect(x: 256, y: 213, width: 160, height: 50) cloud2.image = UIImage(named: "bg-sunny-cloud-2") cloud3.frame = CGRect(x: 284, y: 503, width: 74, height: 35) cloud3.image = UIImage(named: "bg-sunny-cloud-3") cloud4.frame = CGRect(x:22 , y: 545, width: 115, height: 50) cloud4.image = UIImage(named: "bg-sunny-cloud-4") } }Copy the code

In viewWillAppear, move the x of titleLabel, usernameTextField and passwordTextField to the left of view.bounds.width and set alpha to 0. Finally, move login down 30 and set alpha to 0.

override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) titleLabel.center.x -= view.bounds.width usernameTextField.center.x -= View. The bounds. Width passwordTextField. Center. X = the bounds. The width cloud1. Alpha = 0.0 cloud2. Alpha = 0.0 cloud3. Alpha = 0.0 cloud4.alpha = 0.0 loginbutton.center. Y += 30.0 loginbutton.alpha = 0.0}Copy the code

And then I’m going to do it in viewDidAppear

  • In UIView.animate, move right to the same position so that they move from the left side of the screen to the X we set in the frame
  • Add opacity change animation to cloud
  • Finally, animate the login button with transparency changes and moves up.
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) UIView.animate(withDuration: {self.titlelabel.center. x += self.view.bounds.width} uiView.animate (withDuration: 0.5, delay: 0.3, options: [], animations: { self.usernameTextField.center.x += self.view.bounds.width }, completion: Nil) uiView.animate (withDuration: 0.5, delay: 0.5, options: [], animations: { self.passwordTextField.center.x += self.view.bounds.width }, completion: nil) UIView.animate(withDuration: Animations: {self.cloud1.alpha = 1.0}, completion: nil) uiView.animate (withDuration: animations: {self.cloud1.alpha = 1.0}, completion: nil) 0.5, delay: 0.7, options: [], animations: {self.cloud2.alpha = 1.0}, completion: nil) uiView.animate (withDuration: animations: 0.5, delay: 0.9, options: [], animations: {self.cloud3.alpha = 1.0}, completion: nil) uiView.animate (withDuration: animations: 0.5, delay: 1.1, options: [], animations: {self.cloud4.alpha = 1.0}, completion: nil) uiView.animate (withDuration: animations: 0.5, delay: 0.5, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: [], animations: {self. LoginButton. Center. - y = 30.0 self. LoginButton, alpha = 1.0}, completion: nil)}Copy the code

Now I’m going to add a click animation to the Login button, and to do that, I’m going to add a click response method in viewDidload.

loginButton.addTarget(self, action: #selector(handleLogin), for: .touchUpInside)
Copy the code

And then we need a UIActivityIndicatorView here, create a UIActivityIndicatorView property and initialize it,

  let spinner = UIActivityIndicatorView(style: .whiteLarge)
Copy the code

Set the property in viewDidLoad, and then add it as a child view of the Login button.

Spinner. Frame = CGRect(x: -20.0, y: 6.0, width: 20.0, height: 0) StartAnimating () spinner. Alpha = 0.0 loginButton.addSubView (spinner)Copy the code

Finally, in the click response method, the click animation will increase the width of the loginButton by 80, and then move the loginButton down by 60 to change the color of the loginButton. Then adjust the spinner, resetting the Y midpoint of the Spinner to the loginButton, and animating the opacity change.

@ibAction func login() {view.endediting (true) uiView.animate (withDuration: 1.5, Delay: 0.0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.0, the options: [], animations: {self. LoginButton. Bounds. Size. Width + = 80.0}, completion: Nil) uiView.animate (withDuration: 0.33, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.0, options: [], animations: {self. LoginButton. Center. + y = 60.0 self. LoginButton, backgroundColor = UIColor (red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0) self.spinner. Center = CGPoint(x: 40.0, y: Self. LoginButton. Frame. The size. Height / 2) self spinner. Alpha = 1.0}, completion: nil)}Copy the code

The next step is to animate the status tag.

  • Declare a UIImageView property to display the tag image
  • Declare a UILabel property to display the status text you want to display
  • Declare a string array to hold the text you want to display.
  • Declare a CGPoint to later save the position of the statusPosition
  let status = UIImageView(image: UIImage(named: "banner"))
  let label = UILabel()
  let messages = ["Connecting ...", "Authorizing ...", "Sending credentials ...", "Failed"]
  var statusPosition = CGPoint.zero
Copy the code

In viewDidLoad, we do the Label image and the state Label.

  • Hide the status, set its center point to the center point of the Login button, and add it as a child view of the View.
  • Set the frame, font, and textColor of the label, and add the child view as status after being centered.
  • Save the center point of the current status
Status. isHidden = true status.center = loginbutton.center view.addSubview(status) label.frame = CGRect(x: 0.0, y: Width: status.frame.size.width, height: status.frame.size.height) label.font = UIFont(name: "HelveticaNeue", size: TextColor = UIColor(red: 0.89, green: 0.38, blue: 0.0, alpha: 1.0) label.textalignment =.center status.addSubView (label) statusPosition = status.centerCopy the code

Add a showMessage to add the animation to the state display, and then call that method in the second UIView.animate completion inside the previous handleLogin method.

Func showMessage(index: Int) {label.text = messages[index] uiView. transition(with: status, duration: 0.33, options: [.curveEaseOut, .transitionFlipFromTop], animations: { self.status.isHidden = false }, completion: { _ in }) }Copy the code

Now that the animation is done, we have to do the animation that moves out. Create a removeMessage method that moves status back to the right by the width of the screen, then hides it in Completion, then displays the next state on a call to showMessage after setting its center where it was.

Func removeMessage(index: Int) {uiView.animate (withDuration: 0.33, delay: 0.0, options: [], animations: { self.status.center.x += self.view.frame.size.width }, completion: { _ in self.status.isHidden = true self.status.center = self.statusPosition self.showMessage(index: index+1) }) }Copy the code

ShowMessage does the logical processing here by setting a delay of two seconds before removing the status tag, and then doing the index check to make sure the array doesn’t cross the line. Here, when all the status tags are displayed, we need to call resetForm to restore the state, such as the restoration of a relic of status, such as button.

Func showMessage(index: Int) {label.text = messages[index] uiView. transition(with: status, duration: 0.33, options: [.curveEaseOut, .transitionFlipFromTop], animations: { self.status.isHidden = false }, completion: {_ in // Transition completion delay(2.0) {if index < self.messages.count-1 {self.removemessage (index: index) } else { resetForm() } } }) }Copy the code

Here delay is a delay function

func delay(_ seconds: Double, completion: @escaping ()->Void) {
  DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: completion)
}
Copy the code

Then you need to restore the state:

  • Rehide the status tag in the resetForm and restore its center point
  • Set spinner to hidden
  • Restore the color, size and position of the loginButton
Func resetForm() {uiView. transition(with: status, duration: 0.2, options:.transitionFlipFromTop, animations: { self.status.isHidden = true self.status.center = self.statusPosition }, completion: nil) UIView.animate(withDuration: 0.2, delay: 0.0, options: [], animations: {self.spinner. Center = CGPoint(x: -20.0, y: animations: {self.spinner. 16.0) the self. The spinner. Alpha = 0.0 self. LoginButton. BackgroundColor = UIColor (green, red, 0.63:0.84, blue: 0.35, alpha: 1.0) the self. LoginButton. Bounds. Size. Width - = 80.0 self. LoginButton. Center. - y = 60.0}, completion: nil)}Copy the code

We are almost done here, but we still need to animate the cloud movement.

Add an animateCloud method and call it in viewDidAppear

  • Figure out the desired velocity
  • Calculate the animation time you want according to your position x
  • Add displacement animation
  • When finished, reset to the left of the screen and restart the animation.
func animateCloud(_ cloud: UIImageView) {let cloudSpeed = 60.0 / view.frame.size. Width let duration = (view.frame.size. Width - Cloud.frame.origine.x) * cloudSpeed uiView.animate (withDuration: TimeInterval(duration), delay: 0.0, options: .curveLinear, animations: { cloud.frame.origin.x = self.view.frame.size.width }, completion: { _ in cloud.frame.origin.x = -cloud.frame.size.width self.animateCloud(cloud) }) }Copy the code

So here we go and we’re done

Meanings of each parameter:

  • Duration: indicates the animation execution time.
  • Delay: indicates the animation delay time.
  • UsingSpringWithDamping: the range of parameters is 0.0f to 1.0f, and the smaller the value is, the more obvious the vibration effect of “spring” will be. Can be regarded as the stiffness coefficient of the spring
  • InitialSpringVelocity: Indicates the initial speed of the animation. The higher the value, the faster it moves at first.
  • Options: Optional animation effects, including repetition.
  • Animations: Represents animation content to perform, including opacity gradients, moves, and zooming.
  • Completion: Indicates the content to be executed after the animation is executed.

Complete code:

import UIKit

class ViewController: UIViewController {
        
    let screenWidth = UIScreen.main.bounds.size.width
    let screenHeight = UIScreen.main.bounds.size.height
    let titleLabel = UILabel()
    let backgroundImage = UIImageView()
    let usernameTextField = TextField()
    let passwordTextField = TextField()
    let loginButton = UIButton()
    let cloud1 = UIImageView()
    let cloud2 = UIImageView()
    let cloud3 = UIImageView()
    let cloud4 = UIImageView()
    let spinner = UIActivityIndicatorView(style: .whiteLarge)
    
    let status = UIImageView(image: UIImage(named: "banner"))
    let label = UILabel()
    let messages = ["Connecting ...", "Authorizing ...", "Sending credentials ...", "Failed"]

    var statusPosition = CGPoint.zero


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        view.addSubview(backgroundImage)
        view.addSubview(titleLabel)
        view.addSubview(usernameTextField)
        view.addSubview(passwordTextField)
        view.addSubview(loginButton)
        view.addSubview(cloud1)
        view.addSubview(cloud2)
        view.addSubview(cloud3)
        view.addSubview(cloud4)
        loginButton.addSubview(spinner)
        
        let textFieldWidth = screenWidth - 60
        let buttonWidth = 260
        
        backgroundImage.image = UIImage(named: "bg-sunny")
        backgroundImage.frame = CGRect(x: 0, y: 0, width: screenWidth, height: screenHeight)
      
        titleLabel.text = "Bahama Login"
        titleLabel.textColor = .white
        titleLabel.font = UIFont.systemFont(ofSize: 28)
        let titleWidth = titleLabel.intrinsicContentSize.width
        titleLabel.frame = CGRect(x: (screenWidth - titleWidth) / 2 , y: 120, width: titleWidth, height: titleLabel.intrinsicContentSize.height)
        
        usernameTextField.backgroundColor = .white
        usernameTextField.layer.cornerRadius = 5
        usernameTextField.placeholder = "  Username"
        usernameTextField.frame = CGRect(x: 30, y: 202, width: textFieldWidth, height: 40)
        
        passwordTextField.backgroundColor = .white
        passwordTextField.layer.cornerRadius = 5
        passwordTextField.placeholder = "  Password"
        passwordTextField.frame = CGRect(x: 30, y: 263, width: textFieldWidth, height: 40)
        
        loginButton.frame = CGRect(x: (Int(screenWidth) - buttonWidth) / 2, y: 343, width: buttonWidth, height: 50)
        loginButton.setTitle("Login", for: .normal)
        loginButton.setTitleColor(.red, for: .normal)
        loginButton.layer.cornerRadius = 5
        loginButton.backgroundColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
        loginButton.addTarget(self, action: #selector(handleLogin), for: .touchUpInside)
        
        spinner.frame = CGRect(x: -20.0, y: 6.0, width: 20.0, height: 20.0)
        spinner.startAnimating()
        spinner.alpha = 0.0
        
        
        cloud1.frame = CGRect(x: -120, y: 79, width: 160, height: 50)
        cloud1.image = UIImage(named: "bg-sunny-cloud-1")
        
        cloud2.frame = CGRect(x: 256, y: 213, width: 160, height: 50)
        cloud2.image = UIImage(named: "bg-sunny-cloud-2")
        
        
        cloud3.frame = CGRect(x: 284, y: 503, width: 74, height: 35)
        cloud3.image = UIImage(named: "bg-sunny-cloud-3")
        
        
        cloud4.frame = CGRect(x:22 , y: 545, width: 115, height: 50)
        cloud4.image = UIImage(named: "bg-sunny-cloud-4")
        
        status.isHidden = true
        status.center = loginButton.center
        view.addSubview(status)

        label.frame = CGRect(x: 0.0, y: 0.0, width: status.frame.size.width, height: status.frame.size.height)
        label.font = UIFont(name: "HelveticaNeue", size: 18.0)
        label.textColor = UIColor(red: 0.89, green: 0.38, blue: 0.0, alpha: 1.0)
        label.textAlignment = .center
        status.addSubview(label)

        statusPosition = status.center
        
       
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        titleLabel.center.x -= view.bounds.width
        usernameTextField.center.x -= view.bounds.width
        passwordTextField.center.x -= view.bounds.width
        
        cloud1.alpha = 0.0
        cloud2.alpha = 0.0
        cloud3.alpha = 0.0
        cloud4.alpha = 0.0
        
        loginButton.center.y += 30.0
        loginButton.alpha = 0.0
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        UIView.animate(withDuration: 0.5) {
            self.titleLabel.center.x += self.view.bounds.width
        }
        
        UIView.animate(withDuration: 0.5, delay: 0.3, options: [], animations: {
            self.usernameTextField.center.x += self.view.bounds.width
        }, completion: nil
        )
        
        UIView.animate(withDuration: 0.5, delay: 0.5, options: [], animations: {
            self.passwordTextField.center.x += self.view.bounds.width
        }, completion: nil)
        
        UIView.animate(withDuration: 0.5, delay: 0.5, options: [], animations: {
            self.cloud1.alpha = 1.0
            
        }, completion: nil)
        UIView.animate(withDuration: 0.5, delay: 0.7, options: [], animations: {
            self.cloud2.alpha = 1.0
            
        }, completion: nil)
        UIView.animate(withDuration: 0.5, delay: 0.9, options: [], animations: {
            self.cloud3.alpha = 1.0
            
        }, completion: nil)
        UIView.animate(withDuration: 0.5, delay: 1.1, options: [], animations: {
            self.cloud4.alpha = 1.0
            
        }, completion: nil)
        
        UIView.animate(withDuration: 0.5, delay: 0.5, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0,
                       options: [], animations: {
                        self.loginButton.center.y -= 30.0
                        self.loginButton.alpha = 1.0
        }, completion: nil)
        
        animateCloud(cloud1)
        animateCloud(cloud2)
        animateCloud(cloud3)
        animateCloud(cloud4)
    }
    
    @objc func handleLogin() {
        view.endEditing(true)
        
        UIView.animate(withDuration: 1.5, delay: 0.0, usingSpringWithDamping: 0.2, initialSpringVelocity: 0.0, options: [], animations: {
          self.loginButton.bounds.size.width += 80.0
        }, completion: nil)
        
        UIView.animate(withDuration: 0.33, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.0, options: [], animations: {
          self.loginButton.center.y += 60.0
          self.loginButton.backgroundColor = UIColor(red: 0.85, green: 0.83, blue: 0.45, alpha: 1.0)
          self.spinner.center = CGPoint(
            x: 40.0,
            y: self.loginButton.frame.size.height/2
          )
          self.spinner.alpha = 1.0
        }, completion: { _ in
            self.showMessage(index:0)
        })
    }
    func showMessage(index: Int) {
      label.text = messages[index]

      UIView.transition(with: status, duration: 0.33, options: [.curveEaseOut, .transitionFlipFromTop], animations: {
        self.status.isHidden = false
      }, completion: { _ in
        //transition completion
        delay(2.0) {
          if index < self.messages.count-1 {
            self.removeMessage(index: index)

          } else {
            self.resetForm()
          }
        }
      })
    }
    
    func removeMessage(index: Int) {
      UIView.animate(withDuration: 0.33, delay: 0.0, options: [], animations: {
        self.status.center.x += self.view.frame.size.width
      }, completion: { _ in
        self.status.isHidden = true
        self.status.center = self.statusPosition

        self.showMessage(index: index+1)
      })
    }
    
    func resetForm() {
      UIView.transition(with: status, duration: 0.2, options: .transitionFlipFromTop, animations: {
        self.status.isHidden = true
        self.status.center = self.statusPosition
      }, completion: nil)

      UIView.animate(withDuration: 0.2, delay: 0.0, options: [], animations: {
        self.spinner.center = CGPoint(x: -20.0, y: 16.0)
        self.spinner.alpha = 0.0
        self.loginButton.backgroundColor = UIColor(red: 0.63, green: 0.84, blue: 0.35, alpha: 1.0)
        self.loginButton.bounds.size.width -= 80.0
        self.loginButton.center.y -= 60.0
      }, completion: nil)
    }
    
    func animateCloud(_ cloud: UIImageView) {
      let cloudSpeed = 60.0 / view.frame.size.width
      let duration = (view.frame.size.width - cloud.frame.origin.x) * cloudSpeed
      UIView.animate(withDuration: TimeInterval(duration), delay: 0.0, options: .curveLinear, animations: {
        cloud.frame.origin.x = self.view.frame.size.width
      }, completion: { _ in
        cloud.frame.origin.x = -cloud.frame.size.width
        self.animateCloud(cloud)
      })
    }

}

func delay(_ seconds: Double, completion: @escaping ()->Void) {
  DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: completion)
}

class TextField: UITextField {

    let padding = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)

    override open func textRect(forBounds bounds: CGRect) -> CGRect {
        return bounds.inset(by: padding)
    }

    override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
        return bounds.inset(by: padding)
    }

    override open func editingRect(forBounds bounds: CGRect) -> CGRect {
        return bounds.inset(by: padding)
    }
}
Copy the code