[Trouble Shooting] UIViewPropertyAnimator 백그라운드에서 포그라운드로 돌아왔을 때, 애니메이션 이어하기

 

 

 UIVIewPropertyAnimator을 활용해서 collectionViewCell의 색을 바꾸려고 했다. 그런데 앱이 백그라운드 상태로 갔다가 다시 포그라운드 상태로 돌아왔을 때, 애니메이션이 다 끝난 상태로 되어버리는 버그가 발생을 했다. 이 글에선 버그를 해결한 방법에 대해 설명해보려고 한다.

 

 

버그 발생

 

 아래 영상처럼 백그라운드 상태로 갔다가 다시 포그라운드 상태로 돌아오면 애니메이션이 멈춘 상태가 아니라 결과가 나타난 채로 보여진다. 정확히 왜 이런 버그가 발생하는지 이유는 아직 찾지 못했다. 이유는 앱이 백그라운드로 전환되면 iOS는 리소스를 절약하기 위해 애니메이션을 포함한 다양한 작업을 일시 중지하거나 중단하기 때문이다. 따라서 애니메이션의 상태를 관리하여 해결해야한다.

 

 

 

 

해결 방법 접근

 

 혹시라도 얼마정도 애니메이션을 진행했는지 저장이 안돼서 그런 건지 몰라서 fractionComplete를 애니메이션의 진행도를 기록하는 변수로 받아서 저장을 했다. 그러나 저장을 해도 계속 같은 버그가 발생했다.

 

 추측으로는 UI를 다루는 애니메이션이고 애니메이션 자체가 원본을 복사해 두고 보여주는 형태라 백그라운드 상태로 앱이 바뀌는 순간 '완료' 상태로 바꿔버리는 것 같다. 그래서 기존에 저장한 애니메이션 상태를 가지고 애니메이션 정도를 파악할 수 있으니, 이를 바탕으로 백그라운드 상태로 바뀔 때 중간에 멈춘 상태로 복구한 후 애니메이션 다시 부여한 방법으로 접근을 하였다.

 

 

백그라운드로 갔는지 파악하기

 우선 앱이 백그라운드로 갔는지 파악을 해줘야한다.

	// SceneDelegate
func sceneDidEnterBackground(_ scene: UIScene) {
   NotificationCenter.default.post(name: Notification.Name("SceneResign"), object: nil, userInfo: ["willResign": true])
}

 

SceneDelegate에서 백그라운드로 돌아갔을 때 실행되는 메서드 안에 NotificationCenter를 활용해 백그라운드 상태로 가면 데이터를 전달받을 수 있게 한다.

 

// 애니메이션을 실행한 ViewController에서
override func ViewDidLoad() {
	  NotificationCenter.default.addObserver(self, selector: #selector(sceneResignStatusNotification), name: NSNotification.Name("SceneResign"), object: nil)
}

// MARK: NotificationCenter (백그라운드 상태로 변화할때)
@objc private func sceneResignStatusNotification(notification: NSNotification) {
   if let value = notification.userInfo?["willResign"] as? Bool {
       isBackground = true
       pauseAnimations()
   }
}

그리고 애니메이션을 실행할 뷰컨트롤러에서 Notification을 활용해 백그라운드 상태가 되었는지 아닌지 관찰을 하게 하고, 백그라운드 상태로 바뀔 시 isBackground 변수의 bool값을 true로 바꿔준다.

 

 	// 애니메이션 멈추고 진행율 저장하기
 	private func pauseAnimations() {
        for (index, animator) in animators.enumerated() {
            animator.pauseAnimation()
            animatorProgress[index] = animator.fractionComplete
        }
    }

그리고 백그라운드 상태로 바뀔 때, 애니메이션을 멈추고 진행율을 저장해 준다.

 

애니메이션 다시 시작하기

    // UIViewPropertyAnimator 객체를 저장한 배열
    var animators: [UIViewPropertyAnimator] = []
    // 애니메이션 진행률 저장 
    var animatorProgress: [CGFloat] = []
    
	private func resumeAnimations() {
        
        // 애니메이션이 완료되면 1.0이 아닌 0으로 저장이 되기 때문에, 0이 아닌 애니메이션을 찾음
        guard let lastIndex = animatorProgress.firstIndex(where: { $0 != 0 }) else { return }
        let nextIndex = lastIndex + 1
        // 애니메이션이 표시될 cell 정보가 기억된 배열
        let list = Array(viewModel.outputQuizList.value[viewModel.outputCurrentIndex.value].selectedArea)
        let listLastIndex = list.count - 1
        
        // 백그라운드 상태인지 아닌지 Bool 값으로 구별
        if isBackground {
            
            // 마지막 애니메이션의 보이는 정도 복구
            for index in Array(list[lastIndex].area) {
                let cell = self.mainView.collectionView.cellForItem(at: IndexPath(item: index, section: 0))
                cell?.backgroundColor = .black.withAlphaComponent(1 - animatorProgress[lastIndex])
            }
            // 아직 안보이는 부분 검은색으로 다시 칠하기
            for restIndex in lastIndex + 1...listLastIndex {
                let areaList = list[restIndex]
                let areaIndex = Array(areaList.area)
                for index in areaIndex {
                    let cell = self.mainView.collectionView.cellForItem(at: IndexPath(item: index, section: 0))
                    cell?.backgroundColor = .black
                }
                // 애니메이션 다시 지정
                let animator = UIViewPropertyAnimator(duration: TimeInterval(2), curve: .linear) {
                    for index in areaIndex {
                        let cell = self.mainView.collectionView.cellForItem(at: IndexPath(item: index, section: 0))
                        cell?.backgroundColor = .clear
                    }
                }
                animators[restIndex] = animator
            }

            // 진행율로 애니메이션 남은 시간 계산하기
            let restTime: CGFloat = CGFloat(2) * (1 - animatorProgress[lastIndex])
            // 마지막 애니메이션 진행중인 곳에 애니메이션 주기
            let animator = UIViewPropertyAnimator(duration: Double(restTime), curve: .linear) {
                for index in Array(list[lastIndex].area) {
                    let cell = self.mainView.collectionView.cellForItem(at: IndexPath(item: index, section: 0))
                    cell?.backgroundColor = .clear
                }
            }
            animators[lastIndex] = animator
        }
        // 마지막 애니메이션 시작하기
        animators[lastIndex].startAnimation()
        // 애니메이션이 끝나면 그 다음 애니메이션 시작
        animators[lastIndex].addCompletion { position in
            if position == .end {
                self.startNextAnimation(index: nextIndex)
            }
        }
    }

 

몇 가지 코드를 부가적으로 설명하기 위해 검은색 셀이 5개 있다고 가정해 보자. 1번째 셀이 끝나고 2번째 셀이 50% 진행율일 때 백그라운드 상태로 바뀌어서 진행율을 저장해 놨다고 해보자. 그렇다면 50% 애니메이션이 진행된 채로 복구를 해줘야 해서 cell 백그라운드 색을 변경해 준다. 그리고 아직 애니메이션이 진행되지 않은 3,4,5번 셀의 색을 검은색으로 다시 바꿔준다. 또 그와 동시에 3,4,5번 셀의 색을 바꿔줄 애니메이션을 다시 지정해 준다. 마지막으로 애니메이션의 남은 시간만큼을 지정해서 2번째 셀의 애니메이션을 지정하고 시작한다. 그리고 2번째 셀의 애니메이션이 끝나면 다음 인덱스의 애니메이션을 시작하는 코드이다.

 

    private func startNextAnimation(index: Int) {
        guard index < animators.count else { return }
        
        let animator = animators[index]
        animator.startAnimation()
        
        animator.addCompletion { [weak self] position in
            guard position == .end else { return }
            
            self?.startNextAnimation(index: index + 1)
        }
    }

위 코드는 다음 인덱스의 애니메이션을 시작하는 코드이다. 

 

 

 

 

 

 

 위의 방법으로 애니메이션이 백그라운드 상태로 바뀌었다가 다시 포그라운드 상태로 돌아와도 이어지게 만들었다. 생각보다 애니메이션은 신경 써야 할 부분이 많고, 또 앱을 구현할 때 백그라운드 상태도 고려해줘야 한다는 것을 다시 한번 깨달은 트러블이었다.