개발/Swift

[Swift] 애니메이션 정지 재생 CGAffineTransform

덤벨로퍼 2025. 4. 11. 17:11

애니메이션 정지 재생

탭 이벤트를 적용할떄 탭을 눌렀다가 떼었을때 처리를 위해서는 Tap Gesture보다는

LongPressGesture를 활용하는게 더 간편한다

  let tapGesture = UILongPressGestureRecognizer()
  tapGesture.minimumPressDuration = 0
  optionView.addGestureRecognizer(tapGesture)

minimumPressDuration 을 0으로 지정하면

누르자마자 gesutre.state 가 began 되므로

바로 터치다운 이벤트를 시작할수있다

  switch gestureState {
  case .began:
      optionView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
  case .ended, .cancelled:
      optionView.transform = .identity
  default: return

위는 view의 scale을 0.9로 줄이는 동작이다.

그리고 .identity를 적용하면 다시 원래대로 돌아온다

만약 진행중인 애니메이션이 있다면 애니메이션을 멈첬다 재생할수있다.

해당 view의 layer 를 가지고 적용시켜준다.

speed를 0으로 지정하여 애니메이션이 멈추게하고 (1이면 원래속도)

timeOffset 을 지금 시간으로 지정해준다.

    private func pauseAnimation(layer: CALayer) {
        let pausedTime = layer.convertTime(CACurrentMediaTime(), from: nil)
        layer.speed = 0
        layer.timeOffset = pausedTime
    }

    private func resumeAnimation(layer: CALayer) {
        let pausedTime: CFTimeInterval = layer.timeOffset
        layer.speed = 1.0
        layer.timeOffset = 0.0
        layer.beginTime = 0.0
        let timeSincePause: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
        layer.beginTime = timeSincePause
    }

beginTime 을 레이어에 저장해놨던 시간으로 바꾸고 스피드를 다시 돌려놔서 재생할수 있다.

다른 제스쳐와 충돌

        optionView.rx.controlEvent(.touchUpInside)
            .do(onNext: { [weak self] in
                self?.delegate?.sendBenefitMissionSucceed(type: .card)
            })
            .map { Reactor.Action.selectButton(id) }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)

그런데 이미 다른곳에서 제스처를 사용중이었는데

LongPress의 minimum duration이 0이라 바로 동작하므로

기존 이벤트 동작이 이루어지지 않았다

minimum duration 을 올려서 해결할수 있지만 그러면 늦게 반응이 와서 좋지 않았다

위같은 경우 같은 Gesture에서 터치업 이벤트를 처리해야했다

    private func handleTapOptionView(gesture: UILongPressGestureRecognizer, optionView: UIView) {
        if case .ended = gesture.state,
           let id = (optionView as? BenefitQuizOptionView)?.id,
           let gestureView = gesture.view {
            let location = gesture.location(in: gestureView)
            if gestureView.bounds.contains(location) { // 위치 테스팅
                delegate?.sendBenefitMissionSucceed(type: .card)
                reactor?.action.onNext(.selectButton(id))
            }
        }

정상적으로 터치 업 할 경우에만 ended 가 될줄 알았는데 그게 아니라 누르고 바깥으로 빼도 ended가 호출 되었다

그래서 원하는 방향으로 동작하지 않았다.

그러므로 gesture.view의 위치 테스팅을 통해서 end 상태에서 위치가 해당 뷰 안에있는지 체크해줘야

올바르게 touch up 된거라 볼수있다.

기존 애니메이션 붕괴

해당 LongPress 이벤트가 있기전에 해당 뷰에 다른 애니메이션이 적용되어있었다

UIView.animateKeyframes(withDuration: withDuration, delay: 0, options: keyframeAnimationOptions) {
      UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: Metric.Animation.labelDuration / withDuration) {
          self.labelContainer.transform = CGAffineTransform(translationX: 0, y: Metric.Animation.labelContainerTranslationY)
          self.labelContainer.layer.opacity = 1
      }
      optionViews.enumerated().forEach { index, view in
          let startTime = Metric.Animation.optionStartTime + eachOptionDelay * Double(index)

          UIView.addKeyframe(withRelativeStartTime: startTime / withDuration, relativeDuration: Metric.Animation.optionDuration / withDuration) {
              view.transform = CGAffineTransform(translationX: 0, y: Metric.Animation.optionTranslationY)
              view.layer.opacity = 1
          }
      }
      let labelStartTime = withDuration - Metric.Animation.participantLabelDuration
      UIView.addKeyframe(withRelativeStartTime: labelStartTime / withDuration, relativeDuration: Metric.Animation.participantLabelDuration) {
          self.participantsLabel.layer.opacity = 1
      }
  } completion: { _ in
      if !isMultipleOptionAnimation {
          self.startOXRepeatAnimation(optionViews: optionViews)
      }
      self.addEventToOptionView(optionViews: optionViews) // LongPress 이벤트 주는곳
  }

복잡해 보이지만 결국 CGAffineTransform 을 사용하여 y 위치를 위로 올린것이다.

근데 아까 pauseAnimation 함수에서

 switch gestureState {
  case .began:
      optionView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
  case .ended, .cancelled:
      optionView.transform = .identity
  default: return

view의 transform 자체를 바꿔버렸다. 그러므로 기존 transform 이 사라지는 것이다

기존 tramsform을 유지하고 바꿀거만 (사이즈만) 바꿔줘야한다.

 switch gestureState {
  case .began:
      optionView.transform = optionView.transform.scaledBy(x: Metric.Animation.touchScale, y: Metric.Animation.touchScale)
  case .ended, .cancelled:
    optionView.transform = optionView.transform.scaledBy(x: 1.0 / Metric.Animation.touchScale, y: 1.0 / Metric.Animation.touchScale)  
    
    default: return

scaledBy 만 수정하여 해결