[Разработка под iOS, Разработка мобильных приложений, Интерфейсы, Swift] Настало время офигительных историй. Кастомные транзишены в iOS. [2/2] 
    
    
        
    
    
    
    
            
    
        
            
                
                                    
                
                                    
                
                    
                
            
        
    
    
        
            
                
                
                    
                           
                    
                        Автор 
                        Сообщение 
                    
                                        
                        
                            
                                
                                
                                                                                                            news_bot ®
                                                                        
                                                                                                                                                
                                                                            
                                                                                                                
                                            Стаж: 7 лет 8 месяцев                                        
                                                                                                                
                                            Сообщений: 27286                                        
                                                                                                                                                
                                                             
                            
                                
                             
                         
                        
                            
                                
                                    
                                        
                                        
 
В прошлой статье мы реализовали анимацию ZoomIn/ZoomOut для открытия и закрытия экрана с историями.В этот раз мы прокачаем StoryBaseViewController и реализуем кастомные анимации при переходе между историями.Навигация между историямиДавайте сделаем анимацию для переходов между историями. enum TransitionOperation {
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: UIPanGestureRecognizer
extension 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
        }
    }
}
- Чтобы наша анимация была интерактивной и следовала за движением пальца, мы создаем объект percentDrivenInteractiveTransition. А operation отвечает за тип перехода (push или pop).
- Добавляем наш жест во view.
- Реализуем обработчик нажатия/свайпа.
- percent отвечает за прогресс свайпа по экрану в диапазоне от 0 до 1.
- В зависимости от состояния жеста конфигурируем наши свойства.
- Как только начинается новый жест, создаем свежий экземпляр UIPercentDrivenInteractiveTransition и сообщаем делегату navigationController’а, что мы самостоятельно его реализуем (реализация будет ниже). Если направление свайпа положительное, то мы сохраняем в переменную operation значение .pop, и сообщаем navigationController’у, что мы начали процесс перехода с анимацией .navigationController?.popViewController(animated: true). Аналогично делаем для .push-перехода.
- Когда наш свайп уже активен, мы передаем его прогресс в percentDrivenInteractiveTransition.
- Если мы просвайпили более половины экрана, или это было сделано с скоростью более 1500, то мы завершаем наш переход percentDrivenInteractiveTransition?.finish(). В противном случае отменяем переход. При этом необходимо очистить percentDrivenInteractiveTransition и navigationController?.delegate.
- В случае отмены свайпа мы также отменяем переход и очищаем значения.
Сейчас при начале свайпа нужно сообщить 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
    }
}
- Этот метод возвращает аниматор для соответствующего перехода.
- Возвращаем объект типа 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
    }
- Получаем view-контроллеры, которые будем анимировать.
- Получаем доступ к представлению containerView, на котором происходит анимация (участвующее в переходе).
- Закругляем углы наших view при переходе.
- width отвечает при анимации за актуальную ширину containerView.
- offsetLeft — начальное положение fromViewController.
- Конфигурируем начальное положение для экранов.
- Перемещаем toViewController.view над/под fromViewController.view, в зависимости от перехода.
- Выставляем финальное положение view-контроллеров для анимации и трансформируем их.
- Убираем любые возможные трансформации и скругления.
- Если переход был отменен, то необходимо удалить всё то, что успели сделать. То есть необходимо удалить toViewController.view из контейнера.
- Сообщаем transitionContext о состоянии перехода.
- Указываем длительность анимации.
Всё, наш аниматор готов. Теперь запускаем проект и наслаждаемся результатом. Анимации работают.
Весь исходный код можете скачать тут. Буду рад вашим комментариям и замечаниям!
===========
 Источник:
habr.com
===========
Похожие новости:
- [Разработка под iOS, Разработка мобильных приложений] Как реализовать таб-бар с нестандартной кнопкой: CAShapeLayer и UIResponderChain
 
- [Платежные системы, Разработка под iOS, Социальные сети и сообщества, Финансы в IT] Clubhouse запускает прямые денежные переводы
 
- [Разработка мобильных приложений, Управление продуктом] Как сократить стоимость мобильной разработки
 
- [Разработка мобильных приложений, Разработка под e-commerce, Карьера в IT-индустрии, IT-компании] Как мобильное приложение помогло «ВкусВиллу» стать лидером по количеству заказов продуктов онлайн
 
- [Информационная безопасность, Разработка под iOS, Разработка мобильных приложений, API, Монетизация мобильных приложений] Apple запрещает использовать рекламные SDK для создания цифрового отпечатка пользователя
 
- [Разработка мобильных приложений, SaaS / S+S, Облачные сервисы, Телемедицина] Доктор в облаке: как мы создали сервис телемедицины для борьбы с коронавирусом в Люксембурге
 
- [Разработка мобильных приложений, Тестирование мобильных приложений] Дайджест релизов мобильной разработки Mail.ru Group за время пандемии
 
- [Разработка под iOS, Разработка мобильных приложений, Разработка игр] Как меня Apple навечно забанил
 
- [Разработка под iOS, Разработка мобильных приложений, Swift] Подключаем нагрудный датчик пульса по Bluetooth на Swift
 
- [Интерфейсы, Биотехнологии, Мозг, Будущее здесь] Уровень 1 Мысленно получаем полезную информацию
Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_interfejsy (Интерфейсы), #_swift, #_citymobil, #_swift, #_ios_development, #_uiviewanimatetransitioning, #_uinavigationcontrollerdelegate, #_uipresentationcontroller, #_blog_kompanii_sitimobil (
Блог компании Ситимобил
), #_razrabotka_pod_ios (
Разработка под iOS
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_interfejsy (
Интерфейсы
), #_swift
                                        
                                        
                                        
                                     
                                    
                                    
                                                                    
                                                                                             
                         
                        
                            
                                                                    
                                                             
                         
                    
                    
                
                
            
        
    
    
    
    
    
            
    
            
    
        
    
    
        
                        Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
    
    
        
        Текущее время: 01-Ноя 00:52
Часовой пояс: UTC + 5 
            
    
                
| Автор | Сообщение | 
|---|---|
| news_bot ® 
                                                                            
                                                                                                                
                                            Стаж: 7 лет 8 месяцев                                         | |
|  В прошлой статье мы реализовали анимацию 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: UIPanGestureRecognizer extension 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 } } } 
 // 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 } } 
 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 } 
  Весь исходный код можете скачать тут. Буду рад вашим комментариям и замечаниям! =========== Источник: habr.com =========== Похожие новости: 
 Блог компании Ситимобил ), #_razrabotka_pod_ios ( Разработка под iOS ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_interfejsy ( Интерфейсы ), #_swift | |
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
    Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 01-Ноя 00:52
Часовой пояс: UTC + 5 
