CGAffineTransformを使ったシャレオツなアニメーション

公開:2020/11/07
更新:2020/11/07
8 min読了の目安(約7200字TECH技術記事

まいどー。今回もSwiftを使ったアニメーションの例を紹介していきます。

今回はCGAffineTransformというAPIを使って、ちょっとおしゃれなログイン画面を作ろうと思います。

完成形

まずは完成形をご覧ください。

この例ではViewのロード時に自動的にアニメーションが走るようになっています。
確認のためにRe-animateボタンを設置して何度でもアニメーションを見れるようにしました。

アニメーション部分のコード

// アニメーションを行う前の準備。
    func prepareForAnimation() {
	// まずはすべてのレイヤーを見えなくする。
        titleLabel.layer.opacity = 0
        imageView.layer.opacity = 0
        appleButton.layer.opacity = 0
        googleButton.layer.opacity = 0
	
        //画面の上の部分。Welcomeの文字と、画像。サイズを小さくする
        titleLabel.layer.setAffineTransform(CGAffineTransform.init(scaleX: 0.8, y: 0.8))
        imageView.layer.setAffineTransform(CGAffineTransform.init(scaleX: 0.8, y: 0.8))
	// ログインボタンは位置を下方向にズラしておく
	appleButton.layer.setAffineTransform(CGAffineTransform.init(translationX: 0, y: 30))
        googleButton.layer.setAffineTransform(CGAffineTransform .init(translationX: 0, y: 60))
    }

    //アニメーション部分。
    func startAnimate() {
        //バネ効果付きのアニメーション。
        UIView.animate(withDuration: 2.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10, options: .curveEaseIn, animations: { [self] in
            //透明度を1にする(=フェードインさせる)
            self.titleLabel.layer.opacity = 1
           //prepareForAnimationで設定したCGAffineTransformを元に戻す。
	   self.titleLabel.layer.setAffineTransform(CGAffineTransform.identity)
            self.imageView.layer.opacity = 1
            self.imageView.layer.setAffineTransform(CGAffineTransform.identity)
            
        }, completion: nil)
        //ボタンの部分のアニメーション。0.6秒遅らせてスタートするので、Welcomeの部分が発火した後にいい感じでフェードインし始める。
        UIView.animate(withDuration: 1, delay: 0.6, options: .curveEaseOut, animations: {
            
            self.appleButton.layer.opacity = 1
           //こちらもズラしたAffineTransformを初期値に戻すことでアニメーションさせる。
	   self.appleButton.layer.setAffineTransform(CGAffineTransform.identity)
            self.googleButton.layer.opacity = 1
            self.googleButton.layer.setAffineTransform(CGAffineTransform.identity)
 
        }, completion: nil)
    }

ポイント解説

今回のアニメーションでは、CGAffineTranform.identyの部分がミソになっています。
prepareForAnimationで、ボタンや画像の位置・サイズを予めズラしておいて、それを「元の状態に戻す」部分をアニメーションさせています。
こうすることで、最終的な位置をまずレイアウトして、そこに動きを後から加えるような作り方ができて直感的です。今回のように画面遷移と同時にビルドインしたいようなアニメーションを作るのに向いているのではないでしょうか。
既存の画面に動きを加えたいような場合にも比較的容易に実装できると思われます。

このようにちょっと一手間加えるだけで、なんでもないログイン画面がプロっぽくなるので、ユーザーにも良い印象を持ってもらえると思います。

おまけ。コード全体

GitHubでコードを公開しています。


//
//  ViewController.swift
//

import UIKit

class ViewController: UIViewController {

    let appleButton = UIButton()
    let googleButton = UIButton()
    let stackView = UIStackView()
    let titleLabel = UILabel()
    let imageView = UIImageView(image: UIImage(named: "topImage"))

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        let resetButton = UIButton()
        resetButton.translatesAutoresizingMaskIntoConstraints = false
        resetButton.setTitle("Re-animate", for: .normal)
        resetButton.addTarget(self, action: #selector(tapResetButton), for: .touchUpInside)
        resetButton.setTitleColor(.systemBlue, for: .normal)
        
        view.addSubview(resetButton)
        
        NSLayoutConstraint.activate([
            resetButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16),
            resetButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        ])
    
        
        setupLoginView()
        setupStackView()
        prepareForAnimation()
        
        view.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            appleButton.widthAnchor.constraint(equalToConstant: 300),
            appleButton.heightAnchor.constraint(equalToConstant: 40),
            googleButton.widthAnchor.constraint(equalToConstant: 300),
            googleButton.heightAnchor.constraint(equalToConstant: 40),
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
        
        startAnimate()
        
    }
    
    func setupLoginView() {
        
        //Title Label
        titleLabel.text = "Welcome"
        titleLabel.font = .boldSystemFont(ofSize: 26)
        titleLabel.numberOfLines = 1
        titleLabel.textAlignment = .center
        
        //Buttons
        appleButton.translatesAutoresizingMaskIntoConstraints = false
        googleButton.translatesAutoresizingMaskIntoConstraints = false
        
        appleButton.setTitle("Login with Apple", for: .normal)
        appleButton.backgroundColor = UIColor.init(red: 0, green: 0, blue: 0, alpha: 1)
        appleButton.layer.cornerRadius = appleButton.intrinsicContentSize.height / 2
        appleButton.layer.cornerCurve = .continuous
        
        googleButton.setTitle("Login with Google", for: .normal)
        googleButton.backgroundColor = UIColor.init(red: 0.129, green: 0.477, blue: 1, alpha: 1)
        googleButton.layer.cornerRadius = googleButton.intrinsicContentSize.height / 2
        googleButton.layer.cornerCurve = .continuous


    }
    
    func setupStackView() {
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .vertical
        stackView.spacing = 16
        stackView.alignment = .center
        stackView.addArrangedSubview(titleLabel)
        stackView.addArrangedSubview(imageView)
        stackView.addArrangedSubview(appleButton)
        stackView.addArrangedSubview(googleButton)
    }
    
    func prepareForAnimation() {
        titleLabel.layer.opacity = 0
        imageView.layer.opacity = 0
        appleButton.layer.opacity = 0
        googleButton.layer.opacity = 0
        
        titleLabel.layer.setAffineTransform(CGAffineTransform.init(scaleX: 0.8, y: 0.8))
        imageView.layer.setAffineTransform(CGAffineTransform.init(scaleX: 0.8, y: 0.8))
        appleButton.layer.setAffineTransform(CGAffineTransform.init(translationX: 0, y: 30))
        googleButton.layer.setAffineTransform(CGAffineTransform .init(translationX: 0, y: 60))
    }
    
    func startAnimate() {
        UIView.animate(withDuration: 2.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10, options: .curveEaseIn, animations: { [self] in
            
            self.titleLabel.layer.opacity = 1
            self.titleLabel.layer.setAffineTransform(CGAffineTransform.identity)
            self.imageView.layer.opacity = 1
            self.imageView.layer.setAffineTransform(CGAffineTransform.identity)
            
        }, completion: nil)
        
        UIView.animate(withDuration: 1, delay: 0.6, options: .curveEaseOut, animations: {
            
            self.appleButton.layer.opacity = 1
            self.appleButton.layer.setAffineTransform(CGAffineTransform.identity)
            self.googleButton.layer.opacity = 1
            self.googleButton.layer.setAffineTransform(CGAffineTransform.identity)
 
        }, completion: nil)
    }
    
    @objc func tapResetButton() {
        prepareForAnimation()
        startAnimate()
    }


}