관리 메뉴

찹모찌의 기록일지

AutoLayout 애니메이션 적용하기 본문

iOS

AutoLayout 애니메이션 적용하기

찹모찌 2024. 4. 30. 23:38

시작하기 앞서 참고한 블로그를 명시합니다.

https://ios-development.tistory.com/908

 

[iOS - swift] Animation 테크닉 - 오토레이아웃, 단일 애니메이션, 연속 애니메이션, 스프링, 뒤집기 (au

* 예제에서 UI 레이아웃을 코드로 편리하게 작성하기 위해 SnapKit 프레임워크 사용 (SnapKit 필수로 알아야 하는 것 포스팅 글 참고) 애니메이션에 사용될 뷰 준비 animationTargetView 선언 // ViewController.s

ios-development.tistory.com

지금까지 AutoLayout은 Animation 적용이 제한적인 걸로 알고 있었는데, 우연찮게 한 블로그 글을 보고 작성합니다.

기존에는 AutoLayout 적용을 못시켜 다른 방법으로 애니메이션이 적용되는 것 처럼 보이게 했었는데(이전글), AutoLayout이 변경되는 것을 애니메이션화 할 수 있는 새로운 방법을 알게되었습니다.

먼저 기존 코드 입니다. UIView.animations에서 constraint를 변경합니다.

//
//  ViewController.swift
//  AnimationTest
//
//  Created by 황지웅 on 4/30/24.
//

import UIKit

final class ViewController: UIViewController {

    private lazy var animationView: UIView = {
        let view = UIView()
        view.backgroundColor = .systemBlue
        view.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(view)
        return view
    }()

    private lazy var animationButton: UIButton = {
        let button = UIButton()
        button.setTitle("애니메이션", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(showAnimation), for: .touchUpInside)
        self.view.addSubview(button)
        return button
    }()

    private var animationValue: Bool = false

    private lazy var constraint1: NSLayoutConstraint? = animationView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 50)
    private lazy var constraint2: NSLayoutConstraint? = animationView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20)
    private lazy var constraint3: NSLayoutConstraint? = animationView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
    private lazy var constraint4: NSLayoutConstraint? = animationView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10)

    private lazy var constraint5: NSLayoutConstraint? = animationView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 200)
    private lazy var constraint6: NSLayoutConstraint? = animationView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 300)
    private lazy var constraint7: NSLayoutConstraint? = animationView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -300)
    private lazy var constraint8: NSLayoutConstraint? = animationView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -200)

    override func viewDidLoad() {
        super.viewDidLoad()
        NSLayoutConstraint.activate([
            animationButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            animationButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
        ])
        setConstraints()
    }

    private func setConstraints() {
        constraint5?.isActive = false
        constraint6?.isActive = false
        constraint7?.isActive = false
        constraint8?.isActive = false
        constraint1?.isActive = true
        constraint2?.isActive = true
        constraint3?.isActive = true
        constraint4?.isActive = true
    }

    private func setConstraints2() {
        constraint5?.isActive = true
        constraint6?.isActive = true
        constraint7?.isActive = true
        constraint8?.isActive = true
    }

    @objc private func showAnimation() {
        if !animationValue {
            UIView.animate(withDuration: 0.5, animations: {
            	self.setConstraints2()
            })
        } else {
            UIView.animate(withDuration: 0.5, animations: {
            	self.setConstraints()
            })
        }
        animationValue = !animationValue
    }

}

결과입니다.

AutoLayout 애니메이션이 적용되지 않음

여기서 layoutIfNeeded를 이용하면 애니메이션을 적용할 수 있습니다.

//
//  ViewController.swift
//  AnimationTest
//
//  Created by 황지웅 on 4/30/24.
//

import UIKit

final class ViewController: UIViewController {

    private lazy var animationView: UIView = {
        let view = UIView()
        view.backgroundColor = .systemBlue
        view.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(view)
        return view
    }()

    private lazy var animationButton: UIButton = {
        let button = UIButton()
        button.setTitle("애니메이션", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(showAnimation), for: .touchUpInside)
        self.view.addSubview(button)
        return button
    }()

    private var animationValue: Bool = false

    private lazy var constraint1: NSLayoutConstraint? = animationView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 50)
    private lazy var constraint2: NSLayoutConstraint? = animationView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20)
    private lazy var constraint3: NSLayoutConstraint? = animationView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
    private lazy var constraint4: NSLayoutConstraint? = animationView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10)

    private lazy var constraint5: NSLayoutConstraint? = animationView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 200)
    private lazy var constraint6: NSLayoutConstraint? = animationView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 300)
    private lazy var constraint7: NSLayoutConstraint? = animationView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -300)
    private lazy var constraint8: NSLayoutConstraint? = animationView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -200)

    override func viewDidLoad() {
        super.viewDidLoad()
        NSLayoutConstraint.activate([
            animationButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            animationButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
        ])
        setConstraints()
    }

    private func setConstraints() {
        constraint5?.isActive = false
        constraint6?.isActive = false
        constraint7?.isActive = false
        constraint8?.isActive = false
        constraint1?.isActive = true
        constraint2?.isActive = true
        constraint3?.isActive = true
        constraint4?.isActive = true
    }

    private func setConstraints2() {
        constraint5?.isActive = true
        constraint6?.isActive = true
        constraint7?.isActive = true
        constraint8?.isActive = true
    }

    @objc private func showAnimation() {
        if !animationValue {
            setConstraints2()
            UIView.animate(withDuration: 0.5, animations: {
                self.view.layoutIfNeeded()
            })
        } else {
            setConstraints()
            UIView.animate(withDuration: 0.5, animations: {
                self.view.layoutIfNeeded()
            })
        }
        animationValue = !animationValue
    }

}

처음에는 animationView의 constraint가 변경되는데 상위 view의 layoutIfNeeded 적용으로 애니메이션이 변경되는지 궁금했습니다. 그래서 정보를 찾아보았습니다.

layoutIfNeeded 공식 문서

layoutIfNeeded는 subviews의 layout을 즉시 업데이트 합니다. 그래서 view의 subview인 animationView의 constraints 적용이 애니메이션화 되었군요.

또, layoutIfNeeded의 Discussion 부분을 보면 When using Auto Layout, the layout engine updates the position of views as needed to satisfy changes in constraints. 라는 설명을 하고 있습니다.

layoutIfNeeded를 통해 subviews의 constraints를 업데이트 하면서 frame을 변경하고 frame 변경은 애니메이션으로 이어집니다.

기존에 isActive의 변경으로 Auto Layout의 크기를 조절했었는데, 이 부분은 단순 constraint의 변경으로 애니메이션이 적용되지 않음. 따라서 layoutIfNeeded로 애니메이션이 적용될 수 있도록 layout을 업데이트 시켜야 합니다.

찾다보니 isActive보다 constant 값을 변경해서 적용하는 것이 더 낫다고도 하네요!

 

추가적으로 setNeedsLayout도 layout을 업데이트 시키는데 왜 애니메이션이 동작하지 않을까 했는데, setNeedsLayout은 다음 싸이클에, layoutIfNeeded는 즉시 업데이트 시키는 시점의 차이에 따른 결과인 것 같습니다. -> 추가적인 내용을 작성해보도록 하겠습니다.