В iOS, як перетягнути вниз, щоб відхилити модаль?


91

Поширеним способом відхилення модаля є проведення пальцем вниз - як ми дозволяємо користувачеві перетягнути модаль вниз, якщо модаль відстає досить далеко, модаль відхиляється, інакше він анімується назад у вихідне положення?

Наприклад, ми можемо знайти це за допомогою перегляду фотографій додатка Twitter або режиму Snapchat "виявити".

Подібні потоки вказують на те, що ми можемо використовувати UISwipeGestureRecognizer та [self dismissViewControllerAnimated ...], щоб відхилити модальний ВК, коли користувач проводить пальцем вниз. Але це обробляє лише один рух, не дозволяючи користувачеві перетягувати модаль.


Погляньте на власні інтерактивні переходи. Це спосіб, яким ви можете це реалізувати. developer.apple.com/library/prerelease/ios/documentation/UIKit/…
croX

Звернувся до репозиторію github.com/ThornTechPublic/InteractiveModal Роберта Чена та написав клас обгортки / обробника для обробки всього. Більше типового коду не підтримує чотири основних переходи (зверху вниз, знизу вгору, зліва направо та праворуч наліво) із відкидаючими жестами github.com/chamira/ProjSetup/blob/master/AppProject/_BasicSetup/…
Chamira Fernando

@ChamiraFernando, подивився ваш код, і це дуже допомагає. Чи є спосіб зробити так, щоб замість одного було включено кілька напрямків?
Джевон Коуелл,

Я зроблю. Час сьогодні надзвичайно стриманий :(
Чаміра Фернандо,

Відповіді:


93

Я щойно створив навчальний посібник для інтерактивного перетягування модального режиму, щоб його відхилити.

http://www.thorntech.com/2016/02/ios-tutorial-close-modal-dragging/

Спочатку я знайшов цю тему заплутаною, тому підручник розробляє це поетапно.

введіть тут опис зображення

Якщо ви просто хочете запустити код самостійно, це репо:

https://github.com/ThornTechPublic/InteractiveModal

Ось такий підхід я використав:

Перегляд контролера

Ви замінюєте анімацію відхилення на спеціальну. Якщо користувач перетягує модаль, входить interactor.

import UIKit

class ViewController: UIViewController {
    let interactor = Interactor()
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if let destinationViewController = segue.destinationViewController as? ModalViewController {
            destinationViewController.transitioningDelegate = self
            destinationViewController.interactor = interactor
        }
    }
}

extension ViewController: UIViewControllerTransitioningDelegate {
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return DismissAnimator()
    }
    func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactor.hasStarted ? interactor : nil
    }
}

Звільнити Animator

Ви створюєте власний аніматор. Це спеціальна анімація, яку ви пакуєте всередині UIViewControllerAnimatedTransitioningпротоколу.

import UIKit

class DismissAnimator : NSObject {
}

extension DismissAnimator : UIViewControllerAnimatedTransitioning {
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return 0.6
    }

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        guard
            let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
            let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey),
            let containerView = transitionContext.containerView()
            else {
                return
        }
        containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
        let screenBounds = UIScreen.mainScreen().bounds
        let bottomLeftCorner = CGPoint(x: 0, y: screenBounds.height)
        let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)

        UIView.animateWithDuration(
            transitionDuration(transitionContext),
            animations: {
                fromVC.view.frame = finalFrame
            },
            completion: { _ in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
            }
        )
    }
}

Інтерактор

Ви підклас, UIPercentDrivenInteractiveTransitionщоб він міг діяти як ваш автомат. Оскільки доступ до об’єкта взаємодії здійснюють обидва ВК, використовуйте його для відстеження просування панорамування.

import UIKit

class Interactor: UIPercentDrivenInteractiveTransition {
    var hasStarted = false
    var shouldFinish = false
}

Модальний контролер подання

Це відображає стан жестів панорами на виклики методу взаємодії. translationInView() yЗначення визначає , чи перетнув користувач поріг. Коли жест панорамування є .Ended, інтерактор або закінчує, або скасовує.

import UIKit

class ModalViewController: UIViewController {

    var interactor:Interactor? = nil

    @IBAction func close(sender: UIButton) {
        dismissViewControllerAnimated(true, completion: nil)
    }

    @IBAction func handleGesture(sender: UIPanGestureRecognizer) {
        let percentThreshold:CGFloat = 0.3

        // convert y-position to downward pull progress (percentage)
        let translation = sender.translationInView(view)
        let verticalMovement = translation.y / view.bounds.height
        let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
        let downwardMovementPercent = fminf(downwardMovement, 1.0)
        let progress = CGFloat(downwardMovementPercent)
        guard let interactor = interactor else { return }

        switch sender.state {
        case .Began:
            interactor.hasStarted = true
            dismissViewControllerAnimated(true, completion: nil)
        case .Changed:
            interactor.shouldFinish = progress > percentThreshold
            interactor.updateInteractiveTransition(progress)
        case .Cancelled:
            interactor.hasStarted = false
            interactor.cancelInteractiveTransition()
        case .Ended:
            interactor.hasStarted = false
            interactor.shouldFinish
                ? interactor.finishInteractiveTransition()
                : interactor.cancelInteractiveTransition()
        default:
            break
        }
    }

}

4
Ей Роберте, чудова робота. У вас є ідея, як ми могли б це змінити, щоб дозволити йому працювати з поданнями таблиці? Тобто, мати можливість знизити, щоб звільнити, коли табличний перегляд знаходиться вгорі? Дякую
Росс Барбіш

1
Росс, я створив нову гілку, яка має робочий приклад: github.com/ThornTechPublic/InteractiveModal/tree/Ross . Якщо ви хочете побачити, як це виглядає спочатку, перегляньте цей GIF: raw.githubusercontent.com/ThornTechPublic/InteractiveModal/… . У поданні таблиці є вбудований panGestureRecognizer, який можна підключити до існуючого методу handleGesture (_ :) за допомогою цільової дії. Щоб уникнути конфлікту із звичайною прокруткою таблиці, випадаюче відхилення починається лише тоді, коли таблиця прокручується вгору. Я скористався знімком, а також додав багато коментарів.
Роберт Чен,

Роберт, ще велика робота. Я зробив власну реалізацію, яка використовує існуючі методи панорамування tableView, такі як scrollViewDidScroll, scrollViewWillBeginDragging. Він вимагає, щоб у tableView відскоки та відмови Вертикально обидва були встановлені в значення true - таким чином ми можемо виміряти ContentOffset елементів табличного перегляду. Перевага цього методу полягає в тому, що він, здається, дозволяє вивести таблицю з екрана одним жестом, якщо є достатня швидкість (через стрибки). Можливо, я надішлю вам запит на витягнення десь цього тижня, обидва варіанти здаються дійсними.
Росс Барбіш,

Хороша робота @RossBarbish, я не можу дочекатися, як я це зробив. Буде дуже солодко мати можливість прокручувати вгору, а потім переходити в інтерактивний режим переходу, все одним плавним рухом.
Роберт Чен,

1
встановити властивість презентації segueto, over current contextщоб уникати чорного екрану ззаду, коли ви опускаєте viewController
nitish005

63

Поділюсь, як я це зробив у Swift 3:

Результат

Впровадження

class MainViewController: UIViewController {

  @IBAction func click() {
    performSegue(withIdentifier: "showModalOne", sender: nil)
  }
  
}

class ModalOneViewController: ViewControllerPannable {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .yellow
  }
  
  @IBAction func click() {
    performSegue(withIdentifier: "showModalTwo", sender: nil)
  }
}

class ModalTwoViewController: ViewControllerPannable {
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .green
  }
}

Де модульні контролери подання успадковуються від того, classщо я створив ( ViewControllerPannable), щоб зробити їх перетяжними та недоступними при досягненні певної швидкості.

Клас ViewControllerPannable

class ViewControllerPannable: UIViewController {
  var panGestureRecognizer: UIPanGestureRecognizer?
  var originalPosition: CGPoint?
  var currentPositionTouched: CGPoint?
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(_:)))
    view.addGestureRecognizer(panGestureRecognizer!)
  }
  
  func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
    let translation = panGesture.translation(in: view)
    
    if panGesture.state == .began {
      originalPosition = view.center
      currentPositionTouched = panGesture.location(in: view)
    } else if panGesture.state == .changed {
        view.frame.origin = CGPoint(
          x: translation.x,
          y: translation.y
        )
    } else if panGesture.state == .ended {
      let velocity = panGesture.velocity(in: view)

      if velocity.y >= 1500 {
        UIView.animate(withDuration: 0.2
          , animations: {
            self.view.frame.origin = CGPoint(
              x: self.view.frame.origin.x,
              y: self.view.frame.size.height
            )
          }, completion: { (isCompleted) in
            if isCompleted {
              self.dismiss(animated: false, completion: nil)
            }
        })
      } else {
        UIView.animate(withDuration: 0.2, animations: {
          self.view.center = self.originalPosition!
        })
      }
    }
  }
}

1
Я скопіював ваш код, і він спрацював. Тільки фон модельного вигляду при знятті чорний, не прозорий, як у вас
Nguy Ann Anh Việt

5
У розкадровці з панелі інспектора атрибутівStoryboard segue of MainViewControllerto ModalViewController: встановіть для властивості Презентація значення Поточний контекст
Вільсон

Це здається простішим за прийняту відповідь, але я отримую помилку в класі ViewControllerPannable. Помилка: "неможливо викликати значення нефункціонального типу UIPanGestureRecognizer". Це в рядку "panGestureRecognizer = UIPanGestureRecognizer (target: self, action: #selector (panGestureAction (_ :)))" Є ідеї?
tylerSF

Виправлена ​​помилка, про яку я згадав, змінивши її на UIPanGestureRecognizer. "panGestureRecognizer = panGestureRecognizer (target ..." змінено на: "panGestureRecognizer = UIPanGestureRecognizer (target ...")
tylerSF

Оскільки я представляю ВК, а не модально, як я можу видалити чорний фон під час відмови?
Містер Бін,

19

Ось однофайлове рішення на основі відповіді @ wilson (спасибі 👍) із наступними вдосконаленнями:


Список удосконалень попереднього рішення

  • Обмежте панорамування, щоб подання лише знизилося:
    • Уникайте горизонтального перекладу, лише оновлюючи yкоординатиview.frame.origin
    • Уникайте панорамирования екрана, коли проводите пальцем вгору let y = max(0, translation.y)
  • Також відхиліть контролер перегляду, виходячи з того, де відпущений палець (за замовчуванням нижня половина екрана), а не лише на основі швидкості проведення пальцем
  • Показувати контролер перегляду як модальний, щоб переконатися, що попередній контролер перегляду відображається позаду, та уникати чорного тла (повинен відповісти на ваше запитання @ nguyễn-anh-việt)
  • Видаліть непотрібне currentPositionTouchedіoriginalPosition
  • Виставити такі параметри:
    • minimumVelocityToHide: якої швидкості досить, щоб приховати (за замовчуванням 1500)
    • minimumScreenRatioToHide: наскільки достатньо низького, щоб приховати (за замовчуванням 0,5)
    • animationDuration : як швидко ми приховуємо / показуємо (за замовчуванням 0,2 с)

Рішення

Свіфт 3 і Свіфт 4:

//
//  PannableViewController.swift
//

import UIKit

class PannableViewController: UIViewController {
    public var minimumVelocityToHide: CGFloat = 1500
    public var minimumScreenRatioToHide: CGFloat = 0.5
    public var animationDuration: TimeInterval = 0.2

    override func viewDidLoad() {
        super.viewDidLoad()

        // Listen for pan gesture
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))
        view.addGestureRecognizer(panGesture)
    }

    @objc func onPan(_ panGesture: UIPanGestureRecognizer) {

        func slideViewVerticallyTo(_ y: CGFloat) {
            self.view.frame.origin = CGPoint(x: 0, y: y)
        }

        switch panGesture.state {

        case .began, .changed:
            // If pan started or is ongoing then
            // slide the view to follow the finger
            let translation = panGesture.translation(in: view)
            let y = max(0, translation.y)
            slideViewVerticallyTo(y)

        case .ended:
            // If pan ended, decide it we should close or reset the view
            // based on the final position and the speed of the gesture
            let translation = panGesture.translation(in: view)
            let velocity = panGesture.velocity(in: view)
            let closing = (translation.y > self.view.frame.size.height * minimumScreenRatioToHide) ||
                          (velocity.y > minimumVelocityToHide)

            if closing {
                UIView.animate(withDuration: animationDuration, animations: {
                    // If closing, animate to the bottom of the view
                    self.slideViewVerticallyTo(self.view.frame.size.height)
                }, completion: { (isCompleted) in
                    if isCompleted {
                        // Dismiss the view when it dissapeared
                        dismiss(animated: false, completion: nil)
                    }
                })
            } else {
                // If not closing, reset the view to the top
                UIView.animate(withDuration: animationDuration, animations: {
                    slideViewVerticallyTo(0)
                })
            }

        default:
            // If gesture state is undefined, reset the view to the top
            UIView.animate(withDuration: animationDuration, animations: {
                slideViewVerticallyTo(0)
            })

        }
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)   {
        super.init(nibName: nil, bundle: nil)
        modalPresentationStyle = .overFullScreen;
        modalTransitionStyle = .coverVertical;
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        modalPresentationStyle = .overFullScreen;
        modalTransitionStyle = .coverVertical;
    }
}

У вашому коді є або друкарська помилка, або відсутня змінна "minimumHeightRatioToHide"
shokaveli

1
Спасибі @shokaveli, виправлено (було minimumScreenRatioToHide)
agirault

Це дуже приємне рішення. Однак у мене є невелика проблема, і я не зовсім впевнений, у чому причина: dropbox.com/s/57abkl9vh2goif8/pannable.gif?dl=0 Червоний фон є частиною модального ВК, синій фон - частиною ВК, який представив модальний. Існує така неприємна поведінка, коли починається розпізнавач жестів панорами, яку я не можу виправити.
Knolraap

1
Привіт @Knolraap. Можливо, подивіться на значення self.view.frame.originперед тим, як зателефонувати sliceViewVerticallyToв перший раз: здається, зсув, який ми бачимо, такий самий, як і висота рядка стану, тож, можливо, ваше початкове походження не дорівнює 0?
agirault

1
Краще використовувати slideViewVerticallyToяк вкладену функцію в onPan.
Нік Ков

16

створив демо-версію для інтерактивного перетягування вниз, щоб відхилити контролер перегляду, як режим виявлення Snapchat. Перевірте цей github для зразка проекту.

введіть тут опис зображення


2
Чудово, але це насправді застаріло. Хтось знає ще один зразок такого проекту?
thelearner

14

Свіфт 4.x, використовуючи Пангестуру

Простий спосіб

Вертикальний

class ViewConrtoller: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDrage(_:))))
    }

    @objc func onDrage(_ sender:UIPanGestureRecognizer) {
        let percentThreshold:CGFloat = 0.3
        let translation = sender.translation(in: view)

        let newX = ensureRange(value: view.frame.minX + translation.x, minimum: 0, maximum: view.frame.maxX)
        let progress = progressAlongAxis(newX, view.bounds.width)

        view.frame.origin.x = newX //Move view to new position

        if sender.state == .ended {
            let velocity = sender.velocity(in: view)
           if velocity.x >= 300 || progress > percentThreshold {
               self.dismiss(animated: true) //Perform dismiss
           } else {
               UIView.animate(withDuration: 0.2, animations: {
                   self.view.frame.origin.x = 0 // Revert animation
               })
          }
       }

       sender.setTranslation(.zero, in: view)
    }
}

Допоміжна функція

func progressAlongAxis(_ pointOnAxis: CGFloat, _ axisLength: CGFloat) -> CGFloat {
        let movementOnAxis = pointOnAxis / axisLength
        let positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0)
        let positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0)
        return CGFloat(positiveMovementOnAxisPercent)
    }

    func ensureRange<T>(value: T, minimum: T, maximum: T) -> T where T : Comparable {
        return min(max(value, minimum), maximum)
    }

Важкий шлях

Перегляньте це -> https://github.com/satishVekariya/DraggableViewController


1
Я намагався використати ваш код. але я хочу зробити невелику зміну, якщо мій підзапис знаходиться внизу, і коли користувач перетягує подання, висота підпрогляду також повинна збільшуватися щодо позиції, яку натискають. Примітка: - жест подія це було поставлено на підвид
Ekra

Звичайно, ви можете це зробити
SPatel

Привіт @SPatel Будь-яка ідея про те, як змінити цей код для використання для перетягування на лівій стороні осі х, тобто е, негативні рухи вздовж осі х?
Nikhil Pandey

1
@SPatel Вам також потрібно змінити заголовок відповіді, оскільки вертикаль відображає переміщення осі x, а горизонталь - переміщення осі y.
Nikhil Pandey

1
Запам'ятайте, modalPresentationStyle = UIModalPresentationOverFullScreenщоб уникнути заднього екрану позаду view.
tounaobun

13

Я придумав надзвичайно простий спосіб зробити це. Просто вставте наступний код у свій контролер перегляду:

Стрімкий 4

override func viewDidLoad() {
    super.viewDidLoad()
    let gestureRecognizer = UIPanGestureRecognizer(target: self,
                                                   action: #selector(panGestureRecognizerHandler(_:)))
    view.addGestureRecognizer(gestureRecognizer)
}

@IBAction func panGestureRecognizerHandler(_ sender: UIPanGestureRecognizer) {
    let touchPoint = sender.location(in: view?.window)
    var initialTouchPoint = CGPoint.zero

    switch sender.state {
    case .began:
        initialTouchPoint = touchPoint
    case .changed:
        if touchPoint.y > initialTouchPoint.y {
            view.frame.origin.y = touchPoint.y - initialTouchPoint.y
        }
    case .ended, .cancelled:
        if touchPoint.y - initialTouchPoint.y > 200 {
            dismiss(animated: true, completion: nil)
        } else {
            UIView.animate(withDuration: 0.2, animations: {
                self.view.frame = CGRect(x: 0,
                                         y: 0,
                                         width: self.view.frame.size.width,
                                         height: self.view.frame.size.height)
            })
        }
    case .failed, .possible:
        break
    }
}

1
Дякую, працює чудово! Просто опустіть Pan Gesture Recogniser до подання у конструкторі інтерфейсів і підключіться до вищезазначеного @IBAction.
balazs630

Працює і в Swift 5. Просто дотримуйтесь інструкцій, які дав @ balazs630.
LondonGuy

Я думаю, що це найкращий спосіб.
Енха

@ Алекс Шубін, Як звільнити при перетягуванні з ViewController на TabbarController?
Vadlapalli Masthan

12

Масово оновлює репо для Swift 4 .

Для Swift 3 я створив наступне, щоб подати a UIViewControllerсправа наліво та відхилити його жестом панорами. Я завантажив це як сховище GitHub .

введіть тут опис зображення

DismissOnPanGesture.swift файл:

//  Created by David Seek on 11/21/16.
//  Copyright © 2016 David Seek. All rights reserved.

import UIKit

class DismissAnimator : NSObject {
}

extension DismissAnimator : UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.6
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        let screenBounds = UIScreen.main.bounds
        let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
        var x:CGFloat      = toVC!.view.bounds.origin.x - screenBounds.width
        let y:CGFloat      = toVC!.view.bounds.origin.y
        let width:CGFloat  = toVC!.view.bounds.width
        let height:CGFloat = toVC!.view.bounds.height
        var frame:CGRect   = CGRect(x: x, y: y, width: width, height: height)

        toVC?.view.alpha = 0.2

        toVC?.view.frame = frame
        let containerView = transitionContext.containerView

        containerView.insertSubview(toVC!.view, belowSubview: fromVC!.view)


        let bottomLeftCorner = CGPoint(x: screenBounds.width, y: 0)
        let finalFrame = CGRect(origin: bottomLeftCorner, size: screenBounds.size)

        UIView.animate(
            withDuration: transitionDuration(using: transitionContext),
            animations: {
                fromVC!.view.frame = finalFrame
                toVC?.view.alpha = 1

                x = toVC!.view.bounds.origin.x
                frame = CGRect(x: x, y: y, width: width, height: height)

                toVC?.view.frame = frame
            },
            completion: { _ in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            }
        )
    }
}

class Interactor: UIPercentDrivenInteractiveTransition {
    var hasStarted = false
    var shouldFinish = false
}

let transition: CATransition = CATransition()

func presentVCRightToLeft(_ fromVC: UIViewController, _ toVC: UIViewController) {
    transition.duration = 0.5
    transition.type = kCATransitionPush
    transition.subtype = kCATransitionFromRight
    fromVC.view.window!.layer.add(transition, forKey: kCATransition)
    fromVC.present(toVC, animated: false, completion: nil)
}

func dismissVCLeftToRight(_ vc: UIViewController) {
    transition.duration = 0.5
    transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    transition.type = kCATransitionPush
    transition.subtype = kCATransitionFromLeft
    vc.view.window!.layer.add(transition, forKey: nil)
    vc.dismiss(animated: false, completion: nil)
}

func instantiatePanGestureRecognizer(_ vc: UIViewController, _ selector: Selector) {
    var edgeRecognizer: UIScreenEdgePanGestureRecognizer!
    edgeRecognizer = UIScreenEdgePanGestureRecognizer(target: vc, action: selector)
    edgeRecognizer.edges = .left
    vc.view.addGestureRecognizer(edgeRecognizer)
}

func dismissVCOnPanGesture(_ vc: UIViewController, _ sender: UIScreenEdgePanGestureRecognizer, _ interactor: Interactor) {
    let percentThreshold:CGFloat = 0.3
    let translation = sender.translation(in: vc.view)
    let fingerMovement = translation.x / vc.view.bounds.width
    let rightMovement = fmaxf(Float(fingerMovement), 0.0)
    let rightMovementPercent = fminf(rightMovement, 1.0)
    let progress = CGFloat(rightMovementPercent)

    switch sender.state {
    case .began:
        interactor.hasStarted = true
        vc.dismiss(animated: true, completion: nil)
    case .changed:
        interactor.shouldFinish = progress > percentThreshold
        interactor.update(progress)
    case .cancelled:
        interactor.hasStarted = false
        interactor.cancel()
    case .ended:
        interactor.hasStarted = false
        interactor.shouldFinish
            ? interactor.finish()
            : interactor.cancel()
    default:
        break
    }
}

Простота використання:

import UIKit

class VC1: UIViewController, UIViewControllerTransitioningDelegate {

    let interactor = Interactor()

    @IBAction func present(_ sender: Any) {
        let vc = self.storyboard?.instantiateViewController(withIdentifier: "VC2") as! VC2
        vc.transitioningDelegate = self
        vc.interactor = interactor

        presentVCRightToLeft(self, vc)
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return DismissAnimator()
    }

    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactor.hasStarted ? interactor : nil
    }
}

class VC2: UIViewController {

    var interactor:Interactor? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        instantiatePanGestureRecognizer(self, #selector(gesture))
    }

    @IBAction func dismiss(_ sender: Any) {
        dismissVCLeftToRight(self)
    }

    func gesture(_ sender: UIScreenEdgePanGestureRecognizer) {
        dismissVCOnPanGesture(self, sender, interactor!)
    }
}

1
Просто кльово! Дякую, що поділились.
Шарад Чаухан,

Привіт, як я можу презентувати за допомогою розпізнавача жестів, будь ласка, допоможіть мені спасибі
Муралікрішна,

Зараз я запускаю канал YouTube із підручниками. Я можу створити епізод, який би висвітлив цю проблему для iOS 13+ / Swift 5
Девід Сік,

6

Те, що ви описуєте, - це інтерактивна спеціальна анімація переходу . Ви налаштовуєте як анімацію, так і жест керування переходом, тобто звільнення (чи ні) представленого контролера перегляду. Найпростіший спосіб його реалізації - комбінування UIPanGestureRecognizer з UIPercentDrivenInteractiveTransition.

У моїй книзі пояснюється, як це зробити, і я розмістив приклади (з книги). Цей конкретний приклад - інша ситуація - перехід відбувається вбік, а не вниз, і це стосується контролера панелі вкладок, а не представленого контролера - але основна ідея точно така ж:

https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch06p296customAnimation2/ch19p620customAnimation1/AppDelegate.swift

Якщо ви завантажите цей проект і запустите його, ви побачите, що те, що відбувається, - це саме те, що ви описуєте, за винятком того, що воно є боковим: якщо перетягування більше половини, ми переходимо, але якщо ні, то скасовуємо та повертаємося до місце.


3
404 Сторінку не знайдено.
траппер

6

Лише вертикальне відхилення

func panGestureAction(_ panGesture: UIPanGestureRecognizer) {
    let translation = panGesture.translation(in: view)

    if panGesture.state == .began {
        originalPosition = view.center
        currentPositionTouched = panGesture.location(in: view)    
    } else if panGesture.state == .changed {
        view.frame.origin = CGPoint(
            x:  view.frame.origin.x,
            y:  view.frame.origin.y + translation.y
        )
        panGesture.setTranslation(CGPoint.zero, in: self.view)
    } else if panGesture.state == .ended {
        let velocity = panGesture.velocity(in: view)
        if velocity.y >= 150 {
            UIView.animate(withDuration: 0.2
                , animations: {
                    self.view.frame.origin = CGPoint(
                        x: self.view.frame.origin.x,
                        y: self.view.frame.size.height
                    )
            }, completion: { (isCompleted) in
                if isCompleted {
                    self.dismiss(animated: false, completion: nil)
                }
            })
        } else {
            UIView.animate(withDuration: 0.2, animations: {
                self.view.center = self.originalPosition!
            })
        }
    }

6

Я створив просте у використанні розширення.

Просто притаманне вашому UIViewController з InteractiveViewController, і ви закінчили InteractiveViewController

виклик методу showInteractive () з вашого контролера, щоб показати як Interactive.

введіть тут опис зображення


4

У цілі C: Ось код

вviewDidLoad

UISwipeGestureRecognizer *swipeRecognizer = [[UISwipeGestureRecognizer alloc]
                                             initWithTarget:self action:@selector(swipeDown:)];
swipeRecognizer.direction = UISwipeGestureRecognizerDirectionDown;
[self.view addGestureRecognizer:swipeRecognizer];

//Swipe Down Method

- (void)swipeDown:(UIGestureRecognizer *)sender{
[self dismissViewControllerAnimated:YES completion:nil];
}

як контролювати час проведення пальцем вниз, перш ніж його скасувати?
TonyTony

2

Ось розширення, яке я зробив на основі відповіді @Wilson:

// MARK: IMPORT STATEMENTS
import UIKit

// MARK: EXTENSION
extension UIViewController {

    // MARK: IS SWIPABLE - FUNCTION
    func isSwipable() {
        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        self.view.addGestureRecognizer(panGestureRecognizer)
    }

    // MARK: HANDLE PAN GESTURE - FUNCTION
    @objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
        let translation = panGesture.translation(in: view)
        let minX = view.frame.width * 0.135
        var originalPosition = CGPoint.zero

        if panGesture.state == .began {
            originalPosition = view.center
        } else if panGesture.state == .changed {
            view.frame.origin = CGPoint(x: translation.x, y: 0.0)

            if panGesture.location(in: view).x > minX {
                view.frame.origin = originalPosition
            }

            if view.frame.origin.x <= 0.0 {
                view.frame.origin.x = 0.0
            }
        } else if panGesture.state == .ended {
            if view.frame.origin.x >= view.frame.width * 0.5 {
                UIView.animate(withDuration: 0.2
                     , animations: {
                        self.view.frame.origin = CGPoint(
                            x: self.view.frame.size.width,
                            y: self.view.frame.origin.y
                        )
                }, completion: { (isCompleted) in
                    if isCompleted {
                        self.dismiss(animated: false, completion: nil)
                    }
                })
            } else {
                UIView.animate(withDuration: 0.2, animations: {
                    self.view.frame.origin = originalPosition
                })
            }
        }
    }

}

ВИКОРИСТАННЯ

Усередині вашого контролера перегляду ви хочете, щоб його можна було зрушувати:

override func viewDidLoad() {
    super.viewDidLoad()

    self.isSwipable()
}

і це буде недопустимо, проводячи пальцем від крайньої лівої сторони контролера перегляду, як навігаційний контролер.


Гей, я використав ваш код, і він чудово працює для проведення пальцем праворуч, але я хочу відмовитись від проведення пальцем вниз, як я можу це зробити? Будь ласка, допоможіть!
Хуш

2

Це мій простий клас для Drag ViewController з осі . Щойно успадкував ваш клас від DraggableViewController.

MyCustomClass: DraggableViewController

Працюйте лише для представленого ViewController.

// MARK: - DraggableViewController

public class DraggableViewController: UIViewController {

    public let percentThresholdDismiss: CGFloat = 0.3
    public var velocityDismiss: CGFloat = 300
    public var axis: NSLayoutConstraint.Axis = .horizontal
    public var backgroundDismissColor: UIColor = .black {
        didSet {
            navigationController?.view.backgroundColor = backgroundDismissColor
        }
    }

    // MARK: LifeCycle

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(onDrag(_:))))
    }

    // MARK: Private methods

    @objc fileprivate func onDrag(_ sender: UIPanGestureRecognizer) {

        let translation = sender.translation(in: view)

        // Movement indication index
        let movementOnAxis: CGFloat

        // Move view to new position
        switch axis {
        case .vertical:
            let newY = min(max(view.frame.minY + translation.y, 0), view.frame.maxY)
            movementOnAxis = newY / view.bounds.height
            view.frame.origin.y = newY

        case .horizontal:
            let newX = min(max(view.frame.minX + translation.x, 0), view.frame.maxX)
            movementOnAxis = newX / view.bounds.width
            view.frame.origin.x = newX
        }

        let positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0)
        let positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0)
        let progress = CGFloat(positiveMovementOnAxisPercent)
        navigationController?.view.backgroundColor = UIColor.black.withAlphaComponent(1 - progress)

        switch sender.state {
        case .ended where sender.velocity(in: view).y >= velocityDismiss || progress > percentThresholdDismiss:
            // After animate, user made the conditions to leave
            UIView.animate(withDuration: 0.2, animations: {
                switch self.axis {
                case .vertical:
                    self.view.frame.origin.y = self.view.bounds.height

                case .horizontal:
                    self.view.frame.origin.x = self.view.bounds.width
                }
                self.navigationController?.view.backgroundColor = UIColor.black.withAlphaComponent(0)

            }, completion: { finish in
                self.dismiss(animated: true) //Perform dismiss
            })
        case .ended:
            // Revert animation
            UIView.animate(withDuration: 0.2, animations: {
                switch self.axis {
                case .vertical:
                    self.view.frame.origin.y = 0

                case .horizontal:
                    self.view.frame.origin.x = 0
                }
            })
        default:
            break
        }
        sender.setTranslation(.zero, in: view)
    }
}

2

Для тих, хто дійсно хоче заглибитися трохи глибше в користувацький перехід UIViewController, я рекомендую цей чудовий підручник від raywenderlich.com .

Оригінальний кінцевий зразок проекту містить помилку. Тож я виправив це та завантажив у репозиторій Github . Проект знаходиться в Swift 5, тому ви можете легко запускати та грати в нього.

Ось попередній перегляд:

І це також інтерактивно!

Щасливого злому!


0

Ви можете використовувати UIPanGestureRecognizer для виявлення перетягування користувача та переміщення ним модального подання. Якщо кінцева позиція знаходиться досить далеко вниз, вигляд можна відхилити або іншим способом анімувати назад до початкового положення.

Перегляньте цю відповідь, щоб отримати додаткову інформацію про те, як реалізувати щось подібне.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.