Проблема UIViewPropertyAnimator с Autolayout

Вот код того, что я пытался повторить по Apple WWDC, но с авторазметкой:

    extension AugmentedReallityViewController {
    @objc func handlePan(recognizer: UIPanGestureRecognizer) {
//        // hide languages and units anyway
//        moveUnitView(show: false)
//        moveLanguageView(show: false)
//
//        let isNowExpanded = settingsPanelState == SettingsPanelState.expanded
//        let newState = isNowExpanded ? SettingsPanelState.collapsed : SettingsPanelState.expanded
//
//        switch recognizer.state {
//        case .began:
//            startInteractiveTransition(state: newState, duration: 1)
//            isLastPanelUpdateToReachTheNewState = true // just in case, but we should change this property later
//        case .changed:
//            let translation = recognizer.translation(in: viewSettings)
//            let fractionComplete = translation.y / viewSettings.frame.size.height
//
//            // we will use this property when interaction ends
//            if fractionComplete != 0 { // if it's == 0 , we need to use prev data
//                isLastPanelUpdateToReachTheNewState = (newState == SettingsPanelState.expanded && fractionComplete < 0) || (newState == SettingsPanelState.collapsed && fractionComplete > 0)
//            }
//
//            updateInteractiveTransition(fractionComplete: fractionComplete)
//        case .ended:
//            continueInteractiveTransition(cancel: !isLastPanelUpdateToReachTheNewState)
//        default:
//            break
//        }
    }

    @objc func handleSettingsTap() {
        // hide languages and units anyway
        moveUnitView(show: false)
        moveLanguageView(show: false)

        let isNowExpanded = settingsPanelState == SettingsPanelState.expanded
        let newState = isNowExpanded ? SettingsPanelState.collapsed : SettingsPanelState.expanded

        animateOrReverseRunningTransition(state: newState, duration: 10)
    }

    // perform all animations with animators if not already running
    private func animateTransitionIfNeeded(state: SettingsPanelState, duration: TimeInterval) {

        if runningAnimators.isEmpty {

//            // define constraint for frame animation
//            // update constraints
//            switch state {
//            case .expanded:
//                constraint_settingsView_bottom.constant = 0
//            case .collapsed:
//                constraint_settingsView_bottom.constant = -constraint_height_settingViewWhitePart.constant
//            }
            // animate that
            let frameAnimator = UIViewPropertyAnimator(duration: duration, curve: .linear, animations: { [weak self] in
                if let strongSelf = self {
                    // define constraint for frame animation
                    // update constraints
                    switch state {
                    case .expanded:
                        strongSelf.constraint_settingsView_bottom.constant = 0
                    case .collapsed:
                        strongSelf.constraint_settingsView_bottom.constant = -(strongSelf.constraint_height_settingViewWhitePart.constant)
                    }
                }

                self?.view.layoutIfNeeded()
            })
            frameAnimator.startAnimation()
            runningAnimators.append(frameAnimator)
            frameAnimator.addCompletion({ [weak self] (position) in
                if position == UIViewAnimatingPosition.end { // need to remove this animator from array
                    if let index = self?.runningAnimators.index(of: frameAnimator) {
                        print("removed animator because of completion")
                        self?.runningAnimators.remove(at: index)
                        // we can change state to a new one
                        self?.settingsPanelState = state
                    }
                    else {
                        print("animator completion with state = \(position)")
                    }
                }
            })
        }
    }

    // starts transition if neccessary or reverses it on tap
    private func animateOrReverseRunningTransition(state: SettingsPanelState, duration: TimeInterval) {
        if runningAnimators.isEmpty { // start transition from start to end
            animateTransitionIfNeeded(state: state, duration: duration)
        }
        else { // reverse all animators
            for animator in runningAnimators {
                animator.stopAnimation(true)
                animator.isReversed = !animator.isReversed
                // test
                print("tried to reverse")
            }
        }
    }

    // called only on pan .begin
    // starts transition if neccessary and pauses (on pan .begin)
    private func startInteractiveTransition(state: SettingsPanelState, duration: TimeInterval) {
        animateTransitionIfNeeded(state: state, duration: duration)
        for animator in runningAnimators {
            animator.pauseAnimation()

            // save progress of any item
            progressWhenInterrupted = animator.fractionComplete
        }
    }

    // scrubs transition on pan .changed
    private func updateInteractiveTransition(fractionComplete: CGFloat) {
        for animator in runningAnimators {
            animator.fractionComplete = fractionComplete + progressWhenInterrupted
        }
    }

    // continue or reverse transition on pan .ended
    private func continueInteractiveTransition(cancel: Bool) {
        for animator in runningAnimators {
            // need to continue or reverse
            if !cancel {
                let timing = UICubicTimingParameters(animationCurve: .easeOut)
                animator.continueAnimation(withTimingParameters: timing, durationFactor: 0)
            }
            else {
                animator.isReversed = true
            }
        }
    }

    private func addPanGustureRecognizerToSettings() {
        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(AugmentedReallityViewController.handlePan(recognizer:)))
//        panGestureRecognizer.cancelsTouchesInView = false
        viewSettings.addGestureRecognizer(panGestureRecognizer)
    }

    private func addTapGestureRecognizerToSettings() {
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(AugmentedReallityViewController.handleSettingsTap))
        tapGestureRecognizer.cancelsTouchesInView = false
        viewSettingsTopTriangle.addGestureRecognizer(tapGestureRecognizer)
    }
}

Сейчас я просто тестирую тап-жесты. И есть 2 основные проблемы:

1) Распознаватель касаний не работает должным образом во время анимации. Но в Apple WWDC они меняли рамки (не ограничения, как в моем случае), и распознаватели касаний работали отлично.

2) Если я изменю обратное свойство, это очень сильно изменит ограничения. У меня есть лишние полоски и так далее

3) Я попробовал оба способа поставить изменяющееся ограничение перед блоком анимации и внутри него. Это не имеет значения, работает так же

Помогите, как это сделать с автомакетом? Или, по крайней мере, как это сделать с фреймами, но мой контроллер представления основан на авторазметке, поэтому в любом случае у меня будут ограничения на этот вид снизу.


person Paul T.    schedule 05.10.2017    source источник


Ответы (2)


Когда вы используете автомакет для анимации, вы делаете это следующим образом:

  1. Убедитесь, что авторазметка выполнена:

    self.view.layoutIfNeeded()
    
  2. Затем вы меняете ограничения ДО блока анимации. Так, например:

    someConstraint.constant = 0
    
  3. Затем, после изменения ограничения, вы сообщаете автомакету, что ограничения были изменены:

    self.view.setNeedsLayout()
    
  4. Затем вы добавляете блок анимации, просто вызывая layoutIfNeeded():

    UIView.animate(withDuration: 1, animations: {
        self.view.layoutIfNeeded()
    })
    

То же самое происходит, когда вы используете UIViewPropertyAnimator — измените ограничения в блоке анимации. Например.:

self.view.layoutIfNeeded()
someConstraint.constant = 0
self.view.setNeedsLayout()
let animator = UIViewPropertyAnimator(duration: 1, curve: .easeInOut) {            
    self.view.layoutIfNeeded()
}
animator.startAnimation()

Это происходит потому, что layoutIfNeeded() выполняет фактическую компоновку — вычисляет кадры затронутых представлений. Поэтому, если вы устанавливаете кадры напрямую, вы устанавливаете их в блоке анимации. Однако Autolayout устанавливает кадры за вас, поэтому вам нужно сообщить автомакету, чтобы он установил их в блоке анимации (как вы бы сделали, если бы установили их напрямую). Вызов layoutIfNeeded() делает именно это — он сообщает механизму автомакета вычислить и установить новые кадры.

Об отмене:

Хотя у меня недостаточно опыта, чтобы быть уверенным на 100%, я ожидаю, что простой установки реверса аниматора будет недостаточно. Поскольку вы применяете ограничения перед запуском анимации, а затем просто указываете автомакету обновлять кадры в соответствии с ограничениями, я бы предположил, что при реверсировании аниматора вам также необходимо будет реверсировать и ограничения, управляющие анимацией.

Аниматор просто анимирует виды в новые кадры. Тем не менее, реверсированные или нет, новые ограничения остаются в силе независимо от того, реверсировали ли вы аниматор или нет. Поэтому после завершения работы аниматора, если позже автомакет снова разметит виды, я ожидаю, что виды попадут в места, установленные текущими активными ограничениями. Проще говоря: Аниматор анимирует смену кадров, но не сами ограничения. Это означает, что реверсивный аниматор переворачивает кадры, но не реверсирует ограничения — как только автомакет выполнит еще один цикл компоновки, они будут снова применены.

person Milan Nosáľ    schedule 05.10.2017
comment
но с таким подходом распознаватель жестов все равно не работает, попробуйте - person Paul T.; 05.10.2017
comment
у вас есть образец проекта, который я могу попробовать? - person Milan Nosáľ; 06.10.2017
comment
извините, я не знаю, но я решил не беспокоиться о взаимодействии во время анимации - person Paul T.; 06.10.2017
comment
Этот подход также работает, когда вы активируете/деактивируете ограничения. Очень полезно. - person dvdblk; 25.08.2018

Важно настроить self.view.layoutIfNeeded() анимацию

private func animateCard(with topOffset: CGFloat) {
        let animator = UIViewPropertyAnimator(duration: 1, curve: .easeOut)

        animator.addAnimations {
            self.topCardConstraint?.constant = topOffset
            self.view.layoutIfNeeded()
        }

        animator.startAnimation()
    }
person Wasim    schedule 04.10.2019