title: iOS complex animation coordination like Android Animator(Set)
body: <p>I've made a fairly complex animation in my Android app using Animator classes. I want to port this animation to iOS. Preferably it is somewhat like the Android Animator. I've looked around and nothing seems to be what I want. The closest I got was with CAAnimation. But unfortunately all child delegates get ignored if they're put in a group.</p>
  
  <p>Let me start with the animation I made on Android. I'm animating three view groups (which contains an ImageView and a TextView). Per button I have an animation which translates the view to the left and simultaneously animate the alpha to 0. After that animation there is another animation which translates the same view in from the right to the original position and also animates the alpha back to 1. There is one view which also has a scale animation besides the translate and alpha animation. All the views are using different timing functions (easing). The animating in and animating out is different and one view has a different timing function for the scale while the alpha and translate animation uses the same. After the first animation ends I'm setting the values to prepare the second animation. The duration of the scale animation is also shorter than the translate and alpha animation. I'm putting the single animations (translate and alpha) inside an AnimatorSet (basically a group for animations). This AnimatorSet is put in another AnimatorSet to run the animations after eachother (first animate and than in). And this AnimatorSet is put in another AnimatorSet which runs the animation of all 3 buttons simultaneously. </p>
  
  <p>Sorry for the long explanation. But this way you understand how I'm trying to port this to iOS. This one is too complex for the UIView.animate(). CAAnimation overrides delegates if put into a CAAnimationGroup. ViewPropertyAnimator doesn't allow custom timing functions to my knowledge and can't coordinate multiple animations.</p>
  
  <p>Does anybody have an idea what I could use for this? I'm also fine with a custom implementation which gives me a callback each animation tick so I can update the view accordingly.</p>
  
  <hr>
  
  <p><strong>Edit</strong></p>
  
  <p>The Android animation code:</p>
  
  <pre><code>fun setState(newState: State) {
      if(state == newState) {
          return
      }
  
      processing = false
  
      val prevState = state
      state = newState
  
      val reversed = newState.ordinal &lt; prevState.ordinal
  
      val animators = ArrayList&lt;Animator&gt;()
      animators.add(getMiddleButtonAnimator(reversed, halfAnimationDone = {
          displayMiddleButtonState()
      }))
  
      if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
          animators.add(getButtonAnimator(leftButton, leftButton, leftButton.imageView.width.toFloat(), reversed, halfAnimationDone = {
              displayLeftButtonState()
          }))
      }
  
      if(prevState == State.TAKE_PICTURE || newState == State.TAKE_PICTURE) {
          animators.add(getButtonAnimator(
              if(newState == State.TAKE_PICTURE) rightButton else null,
              if(newState == State.CROP_PICTURE) rightButton else null,
              rightButton.imageView.width.toFloat(),
              reversed,
              halfAnimationDone = {
                  displayRightButtonState(inAnimation = true)
              }))
      }
  
      val animatorSet = AnimatorSet()
      animatorSet.playTogether(animators)
      animatorSet.start()
  }
  
  fun getButtonAnimator(animateInView: View?, animateOutView: View?, maxTranslationXValue: Float, reversed: Boolean, halfAnimationDone: () -&gt; Unit): Animator {
      val animators = ArrayList&lt;Animator&gt;()
  
      if(animateInView != null) {
          val animateInAnimator = getSingleButtonAnimator(animateInView, maxTranslationXValue, true, reversed)
          if(animateOutView == null) {
              animateInAnimator.addListener(object : AnimatorListenerAdapter() {
                  override fun onAnimationStart(animation: Animator?) {
                      halfAnimationDone()
                  }
              })
          }
          animators.add(animateInAnimator)
      }
  
      if(animateOutView != null) {
          val animateOutAnimator = getSingleButtonAnimator(animateOutView, maxTranslationXValue, false, reversed)
          animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
              override fun onAnimationEnd(animation: Animator?) {
                  halfAnimationDone()
              }
          })
          animators.add(animateOutAnimator)
      }
  
      val animatorSet = AnimatorSet()
      animatorSet.playTogether(animators)
  
      return animatorSet
  }
  
  private fun getSingleButtonAnimator(animateView: View, maxTranslationXValue: Float, animateIn: Boolean, reversed: Boolean): Animator {
      val translateDuration = 140L
      val fadeDuration = translateDuration
  
      val translateValues =
          if(animateIn) {
              if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
              else floatArrayOf(maxTranslationXValue, 0f)
          } else {
              if(reversed) floatArrayOf(0f, maxTranslationXValue)
              else floatArrayOf(0f, -maxTranslationXValue)
          }
      val alphaValues =
          if(animateIn) {
              floatArrayOf(0f, 1f)
          } else {
              floatArrayOf(1f, 0f)
          }
  
      val translateAnimator = ObjectAnimator.ofFloat(animateView, "translationX", *translateValues)
      val fadeAnimator = ObjectAnimator.ofFloat(animateView, "alpha", *alphaValues)
  
      translateAnimator.duration = translateDuration
      fadeAnimator.duration = fadeDuration
  
      if(animateIn) {
          translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
          fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_OUT)
      } else {
          translateAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
          fadeAnimator.interpolator = EasingInterpolator(Ease.CUBIC_IN)
      }
  
      val animateSet = AnimatorSet()
      if(animateIn) {
          animateSet.startDelay = translateDuration
      }
      animateSet.playTogether(translateAnimator, fadeAnimator)
  
      return animateSet
  }
  
  fun getMiddleButtonAnimator(reversed: Boolean, halfAnimationDone: () -&gt; Unit): Animator {
      val animateInAnimator = getMiddleButtonSingleAnimator(true, reversed)
      val animateOutAnimator = getMiddleButtonSingleAnimator(false, reversed)
  
      animateOutAnimator.addListener(object : AnimatorListenerAdapter() {
          override fun onAnimationEnd(animation: Animator?) {
              halfAnimationDone()
          }
      })
  
      val animatorSet = AnimatorSet()
      animatorSet.playTogether(animateInAnimator, animateOutAnimator)
  
      return animatorSet
  }
  
  private fun getMiddleButtonSingleAnimator(animateIn: Boolean, reversed: Boolean): Animator {
      val translateDuration = 140L
      val scaleDuration = 100L
      val fadeDuration = translateDuration
      val maxTranslationXValue = middleButtonImageView.width.toFloat()
  
      val translateValues =
          if(animateIn) {
              if(reversed) floatArrayOf(-maxTranslationXValue, 0f)
              else floatArrayOf(maxTranslationXValue, 0f)
          } else {
              if(reversed) floatArrayOf(0f, maxTranslationXValue)
              else floatArrayOf(0f, -maxTranslationXValue)
          }
      val scaleValues =
          if(animateIn) floatArrayOf(0.8f, 1f)
          else floatArrayOf(1f, 0.8f)
      val alphaValues =
          if(animateIn) {
              floatArrayOf(0f, 1f)
          } else {
              floatArrayOf(1f, 0f)
          }
  
      val translateAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "translationX", *translateValues)
      val scaleXAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleX", *scaleValues)
      val scaleYAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "scaleY", *scaleValues)
      val fadeAnimator = ObjectAnimator.ofFloat(middleButtonImageView, "alpha", *alphaValues)
  
      translateAnimator.duration = translateDuration
      scaleXAnimator.duration = scaleDuration
      scaleYAnimator.duration = scaleDuration
      fadeAnimator.duration = fadeDuration
  
      if(animateIn) {
          translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
          scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
          scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_OUT)
          fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_OUT)
      } else {
          translateAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
          scaleXAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
          scaleYAnimator.interpolator = EasingInterpolator(Ease.CIRC_IN)
          fadeAnimator.interpolator = EasingInterpolator(Ease.QUINT_IN)
      }
  
      if(animateIn) {
          val scaleStartDelay = translateDuration - scaleDuration
          val scaleStartValue = scaleValues[0]
  
          middleButtonImageView.scaleX = scaleStartValue
          middleButtonImageView.scaleY = scaleStartValue
  
          scaleXAnimator.startDelay = scaleStartDelay
          scaleYAnimator.startDelay = scaleStartDelay
      }
  
      val animateSet = AnimatorSet()
      if(animateIn) {
          animateSet.startDelay = translateDuration
      }
      animateSet.playTogether(translateAnimator, scaleXAnimator, scaleYAnimator)
  
      return animateSet
  }
  </code></pre>
  
  <hr>
  
  <p><strong>Edit 2</strong></p>
  
  <p>Here is a video of how the animation looks on Android:</p>
  
  <p><a href="https://youtu.be/IKAB9A9qHic" rel="nofollow noreferrer">https://youtu.be/IKAB9A9qHic</a></p>