Русский
Русский
English
Статистика
Реклама

Настало время офигительных историй. Кастомные транзишены в iOS. 22

В прошлой статье мы реализовали анимацию ZoomIn/ZoomOut для открытия и закрытия экрана с историями.

В этот раз мы прокачаем StoryBaseViewController и реализуем кастомные анимации при переходе между историями.

Навигация между историями

Давайте сделаем анимацию для переходов между историями.

enum TransitionOperation {    case push, pop}public class StoryBaseViewController: UIViewController {        // MARK: - Constants    private enum Spec {        static let minVelocityToHide: CGFloat = 1500                enum CloseImage {            static let size: CGSize = CGSize(width: 40, height: 40)            static var original: CGPoint = CGPoint(x: 24, y: 50)        }    }        // MARK: - UI components    private lazy var closeButton: UIButton = {        let button = UIButton(type: .custom)        button.setImage(#imageLiteral(resourceName: "close"), for: .normal)        button.addTarget(self, action: #selector(closeButtonAction(sender:)), for: .touchUpInside)        button.frame = CGRect(origin: Spec.CloseImage.original, size: Spec.CloseImage.size)        return button    }()        // MARK: - Private properties    // 1    private lazy var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition? = nil    private lazy var operation: TransitionOperation? = nil        // MARK: - Lifecycle    public override func loadView() {        super.loadView()        setupUI()    }    }extension StoryBaseViewController {        private func setupUI() {        // 2        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))        panGestureRecognizer.delegate = self        view.addGestureRecognizer(panGestureRecognizer)        view.addSubview(closeButton)    }        @objc    private func closeButtonAction(sender: UIButton!) {        dismiss(animated: true, completion: nil)    }    }// MARK: UIPanGestureRecognizerextension StoryBaseViewController: UIGestureRecognizerDelegate {        @objc    func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {        handleHorizontalSwipe(panGesture: panGesture)    }        // 3    private func handleHorizontalSwipe(panGesture: UIPanGestureRecognizer) {                let velocity = panGesture.velocity(in: view)        // 4 Отвечает за прогресс свайпа по экрану, в диапазоне от 0 до 1        var percent: CGFloat {            switch operation {            case .push:                return abs(min(panGesture.translation(in: view).x, 0)) / view.frame.width                            case .pop:                return max(panGesture.translation(in: view).x, 0) / view.frame.width                            default:                return max(panGesture.translation(in: view).x, 0) / view.frame.width            }        }                // 5        switch panGesture.state {        case .began:            // 6            percentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition()            percentDrivenInteractiveTransition?.completionCurve = .easeOut                        navigationController?.delegate = self            if velocity.x > 0 {                operation = .pop                navigationController?.popViewController(animated: true)            } else {                operation = .push                                let nextVC = StoryBaseViewController()                nextVC.view.backgroundColor = UIColor.random                navigationController?.pushViewController(nextVC, animated: true)            }                    case .changed:            // 7            percentDrivenInteractiveTransition?.update(percent)                    case .ended:            // 8            if percent > 0.5 || velocity.x > Spec.minVelocityToHide {                percentDrivenInteractiveTransition?.finish()            } else {                percentDrivenInteractiveTransition?.cancel()            }            percentDrivenInteractiveTransition = nil            navigationController?.delegate = nil                    case .cancelled, .failed:            // 9            percentDrivenInteractiveTransition?.cancel()            percentDrivenInteractiveTransition = nil            navigationController?.delegate = nil                    default:            break        }    }    }
  1. Чтобы наша анимация была интерактивной и следовала за движением пальца, мы создаем объект percentDrivenInteractiveTransition. А operation отвечает за тип перехода (push или pop).

  2. Добавляем наш жест во view.

  3. Реализуем обработчик нажатия/свайпа.

  4. percent отвечает за прогресс свайпа по экрану в диапазоне от 0 до 1.

  5. В зависимости от состояния жеста конфигурируем наши свойства.

  6. Как только начинается новый жест, создаем свежий экземпляр UIPercentDrivenInteractiveTransition и сообщаем делегату navigationControllerа, что мы самостоятельно его реализуем (реализация будет ниже). Если направление свайпа положительное, то мы сохраняем в переменную operation значение.pop, и сообщаем navigationControllerу, что мы начали процесс перехода с анимацией .navigationController?.popViewController(animated: true). Аналогично делаем для.push-перехода.

  7. Когда наш свайп уже активен, мы передаем его прогресс в percentDrivenInteractiveTransition.

  8. Если мы просвайпили более половины экрана, или это было сделано с скоростью более 1500, то мы завершаем наш переход percentDrivenInteractiveTransition?.finish(). В противном случае отменяем переход. При этом необходимо очистить percentDrivenInteractiveTransition и navigationController?.delegate.

  9. В случае отмены свайпа мы также отменяем переход и очищаем значения.

Сейчас при начале свайпа нужно сообщить navigationControllerу, что мы реализуем делегат navigationController?.delegate = self. Но мы этого так и не сделали. Самое время:

// MARK: UINavigationControllerDelegate    extension StoryBaseViewController: UINavigationControllerDelegate {        // 1    public func navigationController(        _ navigationController: UINavigationController,        animationControllerFor operation: UINavigationController.Operation,        from fromVC: UIViewController,        to toVC: UIViewController    ) -> UIViewControllerAnimatedTransitioning? {                switch operation {        case .push:            return StoryBaseAnimatedTransitioning(operation: .push)                    case .pop:            return StoryBaseAnimatedTransitioning(operation: .pop)                    default:            return nil        }    }        // 2    public func navigationController(        _ navigationController: UINavigationController,        interactionControllerFor animationController: UIViewControllerAnimatedTransitioning    ) -> UIViewControllerInteractiveTransitioning? {            return percentDrivenInteractiveTransition    }    }
  1. Этот метод возвращает аниматор для соответствующего перехода.

  2. Возвращаем объект типа UIPercentDrivenInteractiveTransition, который отвечает за прогресс интерактивного перехода.

Аниматор

Наконец-то реализуем аниматор, который непосредственно отвечает за поведение перехода.

Нам необходимы два метода делегата, отвечающие за продолжительность анимации и сам переход.

class StoryBaseAnimatedTransitioning: NSObject {        private enum Spec {        static let animationDuration: TimeInterval = 0.3        static let cornerRadius: CGFloat = 10        static let minimumScale = CGAffineTransform(scaleX: 0.85, y: 0.85)    }        private let operation: TransitionOperation        init(operation: TransitionOperation) {        self.operation = operation    }    }extension StoryBaseAnimatedTransitioning: UIViewControllerAnimatedTransitioning {        // http://fusionblender.net/swipe-transition-between-uiviewcontrollers/    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {                /// 1 Получаем view-контроллеры, которые будем анимировать.        guard            let fromViewController = transitionContext.viewController(forKey: .from),            let toViewController = transitionContext.viewController(forKey: .to)        else {            return        }                /// 2 Получаем доступ к представлению, на котором происходит анимация (которое участвует в переходе).        let containerView = transitionContext.containerView        containerView.backgroundColor = UIColor.clear                /// 3 Закругляем углы наших view при переходе.        fromViewController.view.layer.masksToBounds = true        fromViewController.view.layer.cornerRadius = Spec.cornerRadius        toViewController.view.layer.masksToBounds = true        toViewController.view.layer.cornerRadius = Spec.cornerRadius                /// 4 Отвечает за актуальную ширину containerView        // Swipe progress == width        let width = containerView.frame.width        /// 5 Начальное положение fromViewController.view (текущий видимый VC)        var offsetLeft = fromViewController.view.frame        /// 6 Устанавливаем начальные значения для fromViewController и toViewController        switch operation {        case .push:            offsetLeft.origin.x = 0            toViewController.view.frame.origin.x = width            toViewController.view.transform = .identity                    case .pop:            offsetLeft.origin.x = width            toViewController.view.frame.origin.x = 0            toViewController.view.transform = Spec.minimumScale        }                /// 7 Перемещаем toViewController.view над/под fromViewController.view, в зависимости от транзишена        switch operation {        case .push:            containerView.insertSubview(toViewController.view, aboveSubview: fromViewController.view)                    case .pop:            containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)        }                // Так как мы уже определили длительность анимации, то просто обращаемся к ней        let duration = self.transitionDuration(using: transitionContext)                UIView.animate(withDuration: duration, delay: 0, options: .curveEaseIn, animations: {                    /// 8. Выставляем финальное положение view-контроллеров для анимации и трансформируем их.            let moveViews = {                toViewController.view.frame = fromViewController.view.frame                fromViewController.view.frame = offsetLeft            }            switch self.operation {            case .push:                moveViews()                toViewController.view.transform = .identity                fromViewController.view.transform = Spec.minimumScale                            case .pop:                toViewController.view.transform = .identity                fromViewController.view.transform = .identity                moveViews()            }                    }, completion: { _ in                        ///9.  Убираем любые возможные трансформации и скругления            toViewController.view.transform = .identity            fromViewController.view.transform = .identity                        fromViewController.view.layer.masksToBounds = true            fromViewController.view.layer.cornerRadius = 0            toViewController.view.layer.masksToBounds = true            toViewController.view.layer.cornerRadius = 0                 /// 10. Если переход был отменен, то необходимо удалить всё то, что успели сделать. То есть необходимо удалить toViewController.view из контейнера.            if transitionContext.transitionWasCancelled {                toViewController.view.removeFromSuperview()            }                        containerView.backgroundColor = .clear            /// 11. Сообщаем transitionContext о состоянии операции            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)        })            }        // 12. Время длительности анимации    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {        return Spec.animationDuration    }
  1. Получаем view-контроллеры, которые будем анимировать.

  2. Получаем доступ к представлению containerView, на котором происходит анимация (участвующее в переходе).

  3. Закругляем углы наших view при переходе.

  4. width отвечает при анимации за актуальную ширину containerView.

  5. offsetLeft начальное положение fromViewController.

  6. Конфигурируем начальное положение для экранов.

  7. Перемещаем toViewController.view над/под fromViewController.view, в зависимости от перехода.

  8. Выставляем финальное положение view-контроллеров для анимации и трансформируем их.

  9. Убираем любые возможные трансформации и скругления.

  10. Если переход был отменен, то необходимо удалить всё то, что успели сделать. То есть необходимо удалить toViewController.view из контейнера.

  11. Сообщаем transitionContext о состоянии перехода.

  12. Указываем длительность анимации.

Всё, наш аниматор готов. Теперь запускаем проект и наслаждаемся результатом. Анимации работают.

Весь исходный код можете скачать тут. Буду рад вашим комментариям и замечаниям!

Источник: habr.com
К списку статей
Опубликовано: 07.04.2021 12:05:29
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании ситимобил

Разработка под ios

Разработка мобильных приложений

Интерфейсы

Swift

Citymobil

Ios development

Uiviewanimatetransitioning

Uinavigationcontrollerdelegate

Uipresentationcontroller

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru