마지막에 튕긴다.
전에 디스패치큐에 대해 깊게 파고드느라 일시정지 됐을 때 앱이 정지되는 오류 개선을 오늘 하게 되었다.
이것도 너무 오래 걸렸는데 .. 우선 코드를 변경한 부분 부터 살펴볼텐데, 변경 전과 후의 코드를 비교하며 설명해보려한다.
view.bringSubviewToFront(tapButton)
를 통해 tapButton
이 화면에서 가장 위에 위치하도록 보장했다. 기본적으로 뷰는 추가된 순서에 따라 위에서부터 아래로 쌓이게 되다보니 fillingView
가 tapButton
보다 아래에 있는 상태일 수 있다.
view.bringSubviewToFront(tapButton)
는 tapButton
이 다른 뷰들보다 항상 위에 있게 하여, 버튼이 채워지는 애니메이션 동안에도 버튼을 사용자가 누를 수 있도록 만든다. 결과적으로 fillingView
가 화면을 채우는 동안에도 tapButton
이 가려지지 않게 된다.
뷰 계층의 명확한 설정
뷰가 겹칠 때 어떤 뷰가 위에 나타나는지 명확히 설정해야 한다. 그렇지 않으면 사용자가 상호작용하려는 버튼이나 다른 UI 요소가 다른 뷰에 가려져서 보이지 않거나 클릭되지 않을 수 있다.
애니메이션과 상호작용의 분리
fillingView
가 화면을 채우면서 애니메이션을 수행하는 동안, tapButton
이 여전히 사용자의 입력을 받을 수 있도록 만들어야 한다. 그렇지 않으면 사용자가 버튼을 클릭할 수 없게 되기 때문에 애니메이션을 종료한 후에야 버튼을 사용할 수 있게 된다.
이 수정 과정은 fillingView
와 tapButton
의 시각적 위치를 명확하게 구분해주고, 애니메이션이 시작되더라도 버튼이 여전히 정상적으로 작동하도록 해주기 때문에 좀더 직관적이고 자연스러워졌다.
변경 전 : 애니메이션이 DispatchQueue로 3초 후에 시작됨.
변경후 : 타이머가 시작되자마자 즉시 애니메이션이 시작되도록 변경. 3초의 딜레이가 제거되었고 타이머가 시작되면 바로 화면을 채우는 애니메이션도 시작됨.
사실 여기서 변경된 건 3초 후부터 화면 채우기시작 코드였던 디스패치큐 코드 부분을 없애고 바로 애니메이션을 실행하는 코드를 작성한 것 뿐이다.
타이머가 시작되면 실제로 화면을 채우는 애니메이션이 시작되기까지 3초의 지연 시간이 있었다. 이렇게 딜레이를 준 이유는 작은 픽셀 조각이 바닥에 닿기까지 3초라는 시간을 지정했기에 그랬지만 곰곰히 생각해보다 불필요한 딜레이가 생기는 것 같아서 쿨하게 없애버렸다.
변경 전 : 애니메이터가 계속 새로 생성되지 않고, 상태를 제대로 체크하지 않았음.
변경 후: 애니메이터가 이미 존재하거나, 상태가 stopped
인 경우에만 새로 만들도록 변경. 이로 인해 애니메이션을 재시작할 때 기존 애니메이터를 재사용하게 되어 성능이 개선되고 불필요한 애니메이터 생성이 방지됨.
변경 전 코드에서는 애니메이터를 새로 생성할 때 상태를 명확히 정하지 않아서 애니메이터가 계속 새로 생성되는 문제가 있었다.
그러니까 버튼을 누를 때마다 UIViewPropertyAnimator
객체가 반복적으로 생성되면서 불필요한 애니메이션 객체가 쌓일 수 있었고 이건 성능 저하를 일으키는 지름길이었다. 그리고 애니메이터의 상태를 체크하지 않아 애니메이션을 중지했다가 다시 시작할 때 기존 애니메이션을 제대로 재사용하지 못했다.
변경 후의 코드를 자세히 봐보자. 추가된 건 아래의 한 줄이다.
if fillingAnimator == nil || fillingAnimator?.state == .stopped
fillingAnimator
가 nil
일 때 애니메이터가 존재하지 않으면 새로운 애니메이터를 생성하게 된다. 그러면 처음 애니메이션을 시작할 때 애니메이터가 아직 없기 때문에 이 조건이 true
가 될 것이다.
애니메이터의 상태가 .stopped
일 때 애니메이터가 정지(stopped) 상태인 경우, 애니메이터가 더 이상 사용할 수 없는 상태이므로 새로운 애니메이터를 생성한다. UIViewPropertyAnimator
는 완료되거나 취소된 후에는 재사용할 수 없기 때문에, 새로운 애니메이터가 필요하다.
따라서 이미 존재하는 애니메이터가 실행 중인 경우에는 새로운 애니메이터를 생성하지 않고 기존 애니메이터를 그대로 사용하게 된다는 것.
애니메이터의 상태를 체크하는 것은 매우 중요하다고 한다. UIViewPropertyAnimator
에는 여러 상태가 존재하는데 그 중 애니메이터가 일시정지하거나 정지되었는지에 따라 다르게 처리할 수 있다
active
: 애니메이터가 실행 중인 상태.paused
: 일시정지된 상태로, 이때는 startAnimation()으로 재개할 수 있다.stopped
: 애니메이터가 완료되거나 취소된 상태로, 재사용할 수 없다.위 코드를 통해 fillingAnimator
가 아직 존재하지 않거나 정지된 상태에서만 새로운 애니메이터를 만들고, 그 외에는 기존 애니메이터를 계속 재사용할 수 있게 된다. 이렇게 사용하면 성능적인 면에도 큰 이점이 있다고 하니 애니메이션을 이용할 때는 용이하게 써봐야겠다.
변경 전 : 타이머가 실행 중일 때 버튼을 누르면 애니메이션이 정지하고 버튼이 복원되었지만, 타이머를 시작하고 애니메이션을 다시 시작하는 과정이 약간 더 복잡한 구조였음.
변경 후 : 타이머가 실행 도중 버튼을 눌렀을 때 타이머와 애니메이션을 중지하고, 버튼의 투명도를 복원하는 간단한 로직으로 변경.
변경 전에는 타이머와 애니메이션의 상태를 확인하고 중지하거나 다시 시작하는 과정이 크게 차이는 나지 않지만 버튼을 누를 때마다 타이머를 정지하고, 애니메이션을 멈추는 과정에서 애니메이션과 타이머를 동시에 관리하는 로직이 서로 얽혀있다보니 코드를 이해하거나 수정하는 것이 좀 어려웠다.
타이머가 실행 중일 때 버튼을 누르면 타이머와 애니메이션이 모두 중지되고, 버튼의 투명도(alpha)가 원래대로 복원된다. 그리고 타이머가 실행 중이 아닐 때 버튼을 누르면 타이머와 애니메이션이 다시 시작되고 버튼의 투명도가 다시 연하게 변경된다.
이 과정을 단계별로 자세히 보자.
if dropTimer != nil {
dropTimer?.invalidate()
dropTimer = nil
fillingAnimator?.pauseAnimation()
UIView.animate(withDuration: 0.3) {
self.tapButton.alpha = 1
}
}
타이머가 실행 중인지 확인 (dropTimer != nil)
dropTimer
가 nil
이 아니라면, 이 말은 즉 타이머가 실행 중이면 이 코드가 실행된다는 것이다.
타이머를 중지
dropTimer?.invalidate()
를 호출하여 현재 실행 중인 타이머가 더 이상 실행되지 않도록 한다.
애니메이션 중지
fillingAnimator?.pauseAnimation()
을 통해 현재 진행 중인 애니메이션을 일시정지한다. 그 때 채우기 애니메이션은 멈춘 상태로 유지될 것이다.
버튼 투명도 복원
UIView.animate(withDuration: 0.3)
블록을 통해 tapButton의 투명도(alpha)를 1로 설정하여 버튼이 원래 상태로 돌아오게 만든다. 이렇게 타이머와 애니메이션이 모두 중지되고 버튼이 원래 진한 색상으로 돌아온다.
else {
view.backgroundColor = UIColor.color4
UIView.animate(withDuration: 0.3, animations: {
self.tapButton.alpha = 0.5
}, completion: { _ in
self.startTimer()
})
startFillingAnimation()
}
dropTimer
가 nil
이면 타이머가 실행 중이 아니기 때문에 타이머와 애니메이션을 다시 시작하는 코드가 실행된다.
버튼 투명도 변경
UIView.animate(withDuration: 0.3)
화면 내에서 tapButton의 투명도(alpha)를 0.5로 낮춰지게 한다. 실행 중임을 뜻하고자 연하게 처리했다.
타이머 시작
버튼의 투명도가 변경된 후 completion
코드 안에서 self.startTimer()
가 호출되어 타이머가 다시 시작된다. completion
코드는 애니메이션이 끝난 후 실행되기 때문에 버튼의 투명도 애니메이션이 끝난 뒤에 타이머가 시작된다.
애니메이션 시작
startFillingAnimation()
메서드를 호출하여 채우기 애니메이션도 다시 시작되게 된다. 이로써 화면의 채우기 애니메이션과 타이머가 동시에 다시 실행되는 것이다.
픽셀 크기 변경
: 10 -> 30 사이즈로 큼직하게 변경했다.
즉시 시작
: 3초 대기 없이 타이머와 화면 채우기 애니메이션이 바로 시작되므로 사용자가 버튼을 누르면 즉시 반응하는 느낌을 받게 됨.
애니메이터 재사용
: 애니메이터가 이미 존재하면 새로 만들지 않고 재사용하기 때문에 성능이 향상되고 코드가 더 효율적임.
버튼 상태 처리 개선
: 버튼을 클릭했을 때의 동작이 더 직관적이고 간단해졌으며 애니메이션과 타이머가 함께 동작하는 흐름이 명확해짐.
갑분 마우스 피하기 게임
기존에 버튼을 최상위로 가져오도록 하면서 fillingView에게 버튼이 가려지는 현상을 해결했고 항상 화면의 가장 상단에 위치할 수 있게 하여 뷰의 초기화나 계층 구조의 대해 확인해 볼 수 있었다. 그리고 TPA을 누르게 되면 3초 후에 밑에서부터 차오르는 애니메이션을 구상했는데, 굳이 3초 뒤에 하려고 하는 것 보단 즉각적인 반응처럼 보이기 위해 거대한 우박처럼 쏟아지는 애니메이션으로 변경했다. 절대 절대 번거롭게 귀찮아서가 아니라 그게 더 나아보였다.
그리고 기존 코드의 애니메이터 상태를 체크하지 못했어서 일시 정지 후 다시 타이머를 시작하려하면 앱이 튕기는 현상이 있었다. 해당 부분은 애니메이터가 이미 존재하거나 상태가 stopped인 경우에만 새로 생성하도록 수정했고, 애니메이션이 재시작 될 때의 문제는 사라졌다.
오류를 해결해나가는 과정은 정말 간단해보였지만 너무나도 어려웠다. 그래도 수정하면서 코드의 간결화나 효율성에 대해 고민도 좀 하게 되었고, 불필요한 것들은 쿨하게 제거하는 과정을 통해서 그나마 조금이라도 성능 부분에 최적화를 해줄 수 있었던 시간이다. 만들 때 최대한 코드간의 상태관리를 신경써가며 작성해야 할 것 같다.