ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Swift] 애니메이션 정지 재생 CGAffineTransform
    개발/Swift 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 만 수정하여 해결

    댓글

Designed by Tistory.