[UIKit] 런타임 레이아웃 제약조건 변경 및 UIView.animate 정리

팔랑이·2025년 11월 3일

iOS/Swift

목록 보기
78/90

런타임 레이아웃 높이 업데이트

다음 녹음 화면을 구현하면서, phase 전환에 따라 레이아웃 높이가 변경되는 애니메이션을 적용할 필요가 있었다.

phase는 enum으로 관리 중이며, 녹음(record) 상태에서 정지 버튼을 누르면 저장(save) phase로 넘어가면서 왼쪽 뷰가 줄어들고 오른쪽 뷰가 나타나는 형태였다.

이전에도 제약조건을 변경하면서 높이가 줄거나 늘어나는 뷰를 만든 적은 있었지만, 이번 기회에 정리하려고 작성하는 글

참고로 Snapkit 사용했다.

	private var recordContainerHeightConstraint: Constraint?

	// 내용 담고있는 컨테이너 뷰
    private let recordButtonContainerView = UIView().then {
        $0.backgroundColor = .white
        $0.layer.cornerRadius = 10
        $0.layer.borderWidth = 1
        $0.layer.borderColor = UIColor.white.cgColor
    }
    
    // 제약조건
    recordButtonContainerView.snp.makeConstraints {
            $0.top.equalTo(sentenceContainerView.snp.bottom).offset(20)
            $0.leading.trailing.equalToSuperview()
            recordContainerHeightConstraint = $0.height.equalTo(333).constraint
            $0.bottom.equalToSuperview().inset(20)
    }    
    
    // 업데이트 메서드 ( record -> save )
    private func updateUIForSavePhase() {
        // ...
        
        recordContainerHeightConstraint?.update(offset: 100)
        UIView.animate(withDuration: 0.3) { [weak self] in
            self?.scrollView.layoutIfNeeded()
        }
        
        // ...
    }
    
    // 업데이트 메서드 ( save -> normal )
    private func updateUIForNormalPhase() {
        // ...
        
        recordContainerHeightConstraint?.update(offset: 333)
        UIView.animate(withDuration: 0.3) { [weak self] in
            self?.scrollView.layoutIfNeeded()
        } completion: { [weak self] _ in
            self?.sentenceLabel2.isHidden = false
            self?.sentenceForeignLabel2.isHidden = false
            self?.recordButton.isHidden = false
            self?.recordStartBubble.isHidden = false
        }
        
        recordStartBubble.transform = .identity
        recordButton.transform = .identity
        startBubbleAnimation()
    }

높이가 변경될 뷰의 제약조건을 프로퍼티로 선언한 뒤, 초기 제약조건 설정 시 해당 프로퍼티에 높이 값을 연결한다.

이후 변경 시점에서 .update(offset:)으로 값을 갱신하고, UIView.animate 블록 안에서 layoutIfNeeded()를 호출하면 자연스럽게 높이 변화 애니메이션이 적용된다.

record → save 전환 시에는 기존 컴포넌트를 즉시 숨기고(save용 컴포넌트를 바로 표시) 애니메이션을 주는 방식이 자연스러워 그대로 사용했다.
반면 save → normal 전환 시에는 먼저 뷰의 크기가 늘어난 뒤 컴포넌트가 나타나는 게 더 자연스러울 것 같아서 애니메이션 completion 블록에서 isHidden = false 처리를 추가했다.

UIView.animate

녹음 버튼 및 말풍선 애니메이션은 다음과 같이 구현했는데,

// MARK: RecordButton, Bubble Animation
    private func startBubbleAnimation() {
        let targetBubble = currentPhase == .normal ? recordStartBubble : recordedBubbleImageView
        
        UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse, .curveEaseInOut]) {
            targetBubble.transform = CGAffineTransform(translationX: 0, y: 10)
        }
    }
    
    private func startRecordButtonAnimation() {
        let targetButton = recordButton
        
        UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction]) {
            targetButton.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
        }
    } 

겸사겸사 UIView.animate의 동작을 다시 정리했다.

options

  • .repeat : 애니메이션을 무한 반복한다. 단, 기본적으로 autoreverse가 없으면 애니메이션이 끝난 후 상태가 누적된다. ex) 위치 이동이 계속 아래로 누적됨.
  • .autoreverse: 애니메이션이 끝나면 반대로 되돌아가는 효과를 준다.
  • .curveEaseInOut: 시작과 끝은 느리고, 중간은 빠르게 진행되는 효과
  • .curveLinear: 일정한 속도로 움직임 (가속/감속 없음)
  • .curveEaseIn / .curveEaseOut: 시작이 느리고 점점 빨라짐 / 시작이 빠르고 점점 느려짐
  • .allowUserInteraction: 애니메이션이 진행 중일 때도 사용자의 터치를 허용 → 설정하지 않으면 UIButton touchevent 안먹는다...
  • .transitionFlipFromLeft / Right / Top / Bottom: 뷰 전환 시 플립(뒤집기) 애니메이션에 사용 (주로 UIView.transition에서 사용)
  • .beginFromCurrentState: 이전 애니메이션이 진행 중일 때, 현재 상태에서 새 애니메이션을 시작 → 애니메이션 중복 시 부드럽게 연결 가능

transform

  • CGAffineTransform(translationX:y:)
    - translationX: X축으로 이동 거리
    - translationY: Y축으로 이동 거리
  • CGAffineTransform(scaleX:y:)
    - scaleX: 1.1 → 가로 10% 확대
    - scaleY: 1.1 → 세로 10% 확대
  • fineTransform(rotationAngle:): 회전 (라디안 단위)
    ex) rotationAngle: .pi / 4 → 45도 회전
  • CGAffineTransform.identity: 변환 초기화 (원래 상태로 복귀)
    ex) view.transform = .identity

내 경우, normal, record, save 세 phase 간 전환이 반복되다 보니,
save → normal로 돌아올 때는 transform을 초기화해야 애니메이션이 정상적으로 재실행되었다.

recordStartBubble.transform = .identity
recordButton.transform = .identity

위와 같이 transform = .identity로 설정해 변환 상태를 원래대로 되돌려주면 이전의 scale, translation 등이 모두 초기화되어 정상적인 애니메이션 동작을 보장한다.

결과물

profile
정체되지 않는 성장

0개의 댓글