최근에 SwiftUI로 데시벨 관련 서비스를 구현하면서 애니메이션을 다루게 되었는데, 예상치 못한 에러를 발견했다.
단순한 문제일 줄 알고 금방 해결할 수 있을 거라고 믿었으나 알고 보니 완전히 다른 영역의 원인이 있어서 꽤나 당황스러웠던...!
혹여 비슷한 문제가 생길까 싶어 팀원들에게 공유하고자 썼던 글이지만 다른 사람들에게도 도움이 될 듯 해서 남겨본다.
🌀 문제 상황
- 단일 뷰에서 프리뷰로 확인하거나, 네비게이션이 적용되지 않은 상태에서는 애니메이션이 의도한대로 잘 나타났다.
- 그러나, 네비게이션으로 한 번 감싼 후 확인하면 의도한 것과 완전히 다르게 움직이는 에러가 발생했다.
- 단순히 scale 애니메이션만 줬음에도 불구하고, 애니메이션을 적용한 요소들이 모두 위아래로 움직이는 기묘한 현상이 발생했다..!
Circle()
.fill(status == "양호" ? .green : .orange)
.opacity(0.2)
.scaleEffect(isAnimating ? 1.05 : 1.0)
.animation(Animation.easeInOut(duration: 0.8).repeatForever(autoreverses: true), value: isAnimating)
.onAppear {
isAnimating = true
}
}
💦 일단 시도한 방법
처음에는 잘 되던 애니메이션이 갑자기 위아래로 움직이니, 레이아웃의 구성 관련 문제가 있다고 생각했다.
애니메이션을 시작할 때 그 요소의 중심점을 잘못 잡는다거나, 움직임이 밀린다거나 하는 문제일 것이라고 추측했다.
1. Geometry로 중심점 잡기
- 내 가설이 맞는지 확인하고 싶었기에 GeometryReader를 사용해서 position x, y 값을 고정시켜보았다.
- 그러나 좌표를 화면의 중심으로 명확히 고정시켜놨음에도 불구하고 애니메이션이 여전히 의도하지 않은 위아래 움직임으로 나타났다.
- 억지로 가운데로 잡아뒀는데도 해결이 안된다고? 그렇다면 이건 완전히 다른 차원의 문제일지도.. 라는 생각이 엄습했다.
2. 다시 생각해보기: 그 전의 문제인가?
처음엔 당연히 레이아웃 관련 문제인줄 알았으나, 아무리 뷰를 만져봐도 해결될 기미가 보이지 않았다.
그렇다면 혹시 레이아웃이 이미 그려진 후가 아니라, 그리기 이전의 문제가 아닐까?
새로운 가설을 기반으로 구글링을 하다가 StackOverflow에서 관련된 QnA를 발견했다.
애니메이션이 제대로 동작하지 않을 경우, 렌더링 순서 문제일 수 있다는 것이다.
해당 요소가 그려지기 이전에 애니메이션을 트리거하면 문제가 발생할 수 있다는 식의 내용이었다.
해당 답변을 보고 렌더링 순서에 애니메이션이 영향을 받을 수 있겠구나, 라는 힌트를 얻었다.
⚡️ 해결방안: “나야, 비동기” (Feat. DispatchQueue)
DispatchQueue로 비동기 처리하기
기존에는 동작을 넣고 싶은 요소에 .animation으로 움직임을 넣고, 애니메이션의 상태를 관리하는 State를 onAppear에서 트리거해주었다. 이렇게 처리한 이유는 어떠한 액션(ex. 버튼을 누른다거나)에서 발생하는 애니메이션이 아니라, 화면이 그려짐과 동시에 바로 상태를 변경해서 움직임이 나타나게 하고 싶었기 때문이다.
하지만 결국 문제는 뷰에서 초반에 레이아웃을 어떻게 구성할지 계산하고 배치하는 과정을 미처 다 마치기 전에 애니메이션이 트리거되는 것이었다. 따라서 DispatchQueue.main.async 를 사용하여 레이아웃 구성 이후에 애니메이션이 발생하도록 처리했다.
문제의 원인
- 뷰의 초기 렌더링
- onAppear에서 애니메이션을 즉시 시작하게 되면, 뷰가 완전히 렌더링되기 전, 즉 레이아웃이 아직 확정되지 않은 시점에 애니메이션이 적용된다.
- NavigationView의 경우, 네비게이션 바와 관련된 레이아웃 조정이 뷰의 초기 렌더링 단계에서 이루어지는데, 이로 인해 뷰가 제대로 고정되지 않고 위아래로 움직이는 문제가 발생할 수 있다.
- 메인 스레드와 레이아웃 구성
- SwiftUI에서 레이아웃과 관련된 작업은 메인 스레드에서 처리된다.
- 뷰가 처음 나타날 때, 레이아웃 관련 작업이 메인 스레드에서 완료되기 전에 애니메이션이 트리거되면 레이아웃이 제대로 갖춰지지 않은 상태에서 애니메이션이 시작될 수 있다.
- 레이아웃도 아직 장만 제대로 못했는데 애니메이션 하라니까 고장나는 것이다.. 뚝딱뚝딱 (
그러니까 괴롭히지 말고 순서 지정해주자)
해결 방법
- DispatchQueue.main.async를 사용하여 비동기적으로 애니메이션을 약간 지연시키면 된다! SwiftUI가 모든 레이아웃을 구성하고 배치에 관한 계산을 끝낸 뒤에 애니메이션을 시작하도록 그 순서를 보장해주는 작업을 해주는 것이다.
- 이 경우 우선 뷰의 레이아웃을 완전히 설정하고, 그 후에 애니메이션이 실행된다. 명시적으로 레이아웃부터 구성하고 움직임을 트리거하기 때문에 예상치 못한 문제가 발생할 가능성을 최소화해준다.
🎉 해결!
이제 애니메이션이 오작동하는 문제가 안정적으로 해결되었다.
뒷쪽의 두 원과 앞의 프로그레스 바가 위아래로 움직이지 않고 잘 동작한다.
private let gradientCircleAnimation = Animation
.linear(duration: 0.8)
.repeatForever()
// ...
Circle()
.fill(status == "양호" ? .green : .orange)
.opacity(0.2)
.scaleEffect(isAnimating ? 1.05 : 1.0)
.onAppear {
DispatchQueue.main.async {
withAnimation(gradientCircleAnimation) {
isAnimating = true
}
}
}
📚 참고자료
My Animation in SwiftUI is not working properly
SwiftUI | Some examples of modern animations with demo cafe app