Одновременно отображать контроллеры представления с анимацией

Я следую этому удивительному видео, чтобы создать настраиваемый переход для моего проекта, потому что я разрабатываю iPad, поэтому вместо этого представления полного экрана контроллера точки назначения, я хочу, чтобы он занимал половину экрана следующим образом:

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

Мой код настраиваемого класса перехода:

class CircularTransition: NSObject {

var circle = UIView()
var startingPoint = CGPoint.zero {
    didSet {
        circle.center = startingPoint
    }
}
var circleColor = UIColor.white
var duration = 0.4

enum circularTransitionMode: Int {
    case present, dismiss
}
var transitionMode = circularTransitionMode.present    
}

extension CircularTransition: UIViewControllerAnimatedTransitioning {

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return duration
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    let containerView = transitionContext.containerView
    if transitionMode == .present {
        if let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to) {

            var viewCenter = presentedView.center
            var viewSize = presentedView.frame.size


            if UIDevice.current.userInterfaceIdiom == .pad {
                viewCenter = CGPoint(x: viewCenter.x, y: viewSize.height)
                viewSize = CGSize(width: viewSize.width, height: viewSize.height)
            }

            circle = UIView()
            circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
            circle.layer.cornerRadius = circle.frame.size.width / 2
            circle.center = startingPoint
            circle.backgroundColor = circleColor
            circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
            containerView.addSubview(circle)

            presentedView.center = startingPoint
            presentedView.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
            presentedView.alpha = 0
            containerView.addSubview(presentedView)

            UIView.animate(withDuration: duration, animations: {
                self.circle.transform = CGAffineTransform.identity
                presentedView.transform = CGAffineTransform.identity
                presentedView.alpha = 1
                presentedView.center = viewCenter
                }, completion: {(sucess: Bool) in transitionContext.completeTransition(sucess)})
        }
    } else {
        if let returningView = transitionContext.view(forKey: UITransitionContextViewKey.from) {
            let viewCenter = returningView.center
            let viewSize = returningView.frame.size

            circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
            circle.layer.cornerRadius = circle.frame.size.width / 2
            circle.center = startingPoint

            UIView.animate(withDuration: duration + 0.1, animations: {
                self.circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
                returningView.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
                returningView.center = self.startingPoint
                returningView.alpha = 0
                }, completion: {(success: Bool) in
                    returningView.center = viewCenter
                    returningView.removeFromSuperview()
                    self.circle.removeFromSuperview()
                    transitionContext.completeTransition(success)
            })
        }
    }
}

func frameForCircle(withViewCenter viewCenter: CGPoint, size viewSize: CGSize, startPoint: CGPoint) -> CGRect {

    let xLength = fmax(startingPoint.x, viewSize.width - startingPoint.x)
    let yLength = fmax(startingPoint.y, viewSize.height - startingPoint.y)
    let offsetVector = sqrt(xLength * xLength + yLength * yLength) * 2
    let size = CGSize(width: offsetVector, height: offsetVector)

    return CGRect(origin: CGPoint.zero, size: size)

}
}

И часть кода в моем контроллере просмотра:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let secondVC = segue.destination as! ResultViewController
    secondVC.transitioningDelegate = self
    secondVC.modalPresentationStyle = .custom
}

// MARK: - Animation

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

    transtion.transitionMode = .dismiss
    transtion.startingPoint = calculateButton.center
    transtion.circleColor = calculateButton.backgroundColor!
    return transtion
}

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {

    transtion.transitionMode = .present
    transtion.startingPoint = calculateButton.center
    transtion.circleColor = calculateButton.backgroundColor!
    return transtion
}

Но контроллер отображает полный экран.

Ответ 1

Итак, я закончил создание своего ответа. Он принимает другой подход, чем другие ответы, так что несите меня.

Вместо добавления представления контейнера то, что я понял, было бы лучшим способом создать подкласс UIViewController (который я назвал CircleDisplayViewController). Тогда все ваши VC, которые должны иметь эту функциональность, могут наследовать от него (а не от UIViewController).

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

То, как ваши VC могут использовать это, выглядит так:

class AnyViewController: CircleDisplayViewController { 

    /* Only inherit from CircleDisplayViewController, 
      otherwise you inherit from UIViewController twice */

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func showCircle(_ sender: UIButton) {

        openCircle(withCenter: sender.center, radius: nil, resultDataSource: calculator!.iterateWPItems())

        //I'll get to this stuff in just a minute

        //Edit: from talking to Bright Future in chat I saw that resultViewController needs to be setup with calculator!.iterateWPItems()

    }

}

Где showCircle представит ваш ResultViewController с помощью передающего делегата с центром круга в отправляющем центре UIButtons.

Подкласс CircleDisplayViewController таков:

class CircleDisplayViewController: UIViewController, UIViewControllerTransitioningDelegate, ResultDelegate {

    private enum CircleState {
        case collapsed, visible
    }

    private var circleState: CircleState = .collapsed

    private var resultViewController: ResultViewController!

    private lazy var transition = CircularTransition()

    func openCircle(withCenter center: CGPoint, radius: CGFloat?, resultDataSource: ([Items], Int, String)) {

        let circleCollapsed = (circleState == .collapsed)

        DispatchQueue.main.async { () -> Void in

            if circleCollapsed {

                self.addCircle(withCenter: center, radius: radius, resultDataSource: resultDataSource)

            }

        }

    }

    private func addCircle(withCenter circleCenter: CGPoint, radius: CGFloat?, resultDataSource: ([Items], Int, String])) {

        var circleRadius: CGFloat!

        if radius == nil {
            circleRadius = view.frame.size.height/2.0
        } else {
            circleRadius = radius
        }

        //instantiate resultViewController here, and setup delegate etc.

        resultViewController = UIStoryboard.resultViewController()

        resultViewController.transitioningDelegate = self
        resultViewController.delegate = self
        resultViewController.modalPresentationStyle = .custom

        //setup any values for resultViewController here

        resultViewController.dataSource = resultDataSource

        //then set the frame of resultViewController (while also setting endFrame)

        let resultOrigin = CGPoint(x: 0.0, y: circleCenter.y - circleRadius)
        let resultSize = CGSize(width: view.frame.size.width, height: (view.frame.size.height - circleCenter.y) + circleRadius)

        resultViewController.view.frame = CGRect(origin: resultOrigin, size: resultSize)
        resultViewController.endframe = CGRect(origin: resultOrigin, size: resultSize)

        transition.circle = UIView()
        transition.startingPoint = circleCenter
        transition.radius = circleRadius

        transition.circle.frame = circleFrame(radius: transition.radius, center: transition.startingPoint)

        present(resultViewController, animated: true, completion: nil)

    }

    func collapseCircle() { //THIS IS THE RESULT DELEGATE FUNCTIONS

        dismiss(animated: true) {

            self.resultViewController = nil

        }

    }

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

        transition.transitionMode = .dismiss
        transition.circleColor = UIColor.red
        return transition

    }

    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {

        transition.transitionMode = .present
        transition.circleColor = UIColor.red
        return transition

    }

    func circleFrame(radius: CGFloat, center: CGPoint) -> CGRect {
        let circleOrigin = CGPoint(x: center.x - radius, y: center.y - radius)
        let circleSize = CGSize(width: radius*2, height: radius*2)
        return CGRect(origin: circleOrigin, size: circleSize)
    }

}

public extension UIStoryboard {
    class func mainStoryboard() -> UIStoryboard { return UIStoryboard(name: "Main", bundle: Bundle.main) }
}

private extension UIStoryboard {

    class func resultViewController() -> ResultViewController {
        return mainStoryboard().instantiateViewController(withIdentifier: "/* Your ID for ResultViewController */") as! ResultViewController
    }

}

Единственная функция, которая вызывается VC, которая наследуется от DisplayCircleViewController, - openCircle, openCircle имеет аргумент circleCenter (который должен быть вашим центром кнопок, который я предполагаю), необязательный аргумент радиуса (если это значение nil, то значение по умолчанию из половины высоты представления, а затем все, что вам нужно для настройки ResultViewController.

В функции addCircle есть некоторые важные вещи:

вы устанавливаете ResultViewController, но вы должны перед представлением (например, вы готовились к segue),

затем настройте кадр для него (я попытался сделать его областью круга, который виден, но он довольно груб здесь, возможно, стоит поиграть с ним),

то здесь я reset окружность перехода (а не в классе перехода), так что я мог бы установить здесь начальную точку круга, радиус и фрейм.

тогда просто нормальный подарок.

Если вы не установили идентификатор для ResultViewController, вам нужно это сделать (см. расширения UIStoryboard)

Я также изменил функции TransitioningDelegate, поэтому вы не устанавливаете центр круга, потому что, чтобы сохранить его общий, я ставлю эту ответственность на ViewController, который наследует от этого. (см. верхний бит кода)

Наконец, я изменил класс CircularTransition

Я добавил переменную:

var radius: CGFloat = 0.0 //set in the addCircle function above

и изменено animateTransition:

(удалены прокомментированные строки):

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

    let containerView = transitionContext.containerView

    if transitionMode == .present {
        if let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to) {

           ...

           // circle = UIView()
           // circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
           circle.layer.cornerRadius = radius

           ...

        }

    } else {

        if let returningView = transitionContext.view(forKey: UITransitionContextViewKey.from) {

            ...

            // circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)

            ...

        }
    }
}

Наконец, я сделал протокол, чтобы ResultViewController мог отклонить круг

protocol ResultDelegate: class {

    func collapseCircle()

}

class ResultViewController: UIViewController {

    weak var delegate: ResultDelegate!

    var endFrame: CGRect! 

    var dataSource: ([Items], Int, String)! // same as in Bright Future case

    override func viewDidLoad() {
        super.viewDidLoad()


    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    override func viewDidLayoutSubviews() {
        if endFrame != nil {
            view.frame = endFrame
        }
    }

    @IBAction func closeResult(_ sender: UIButton) {

        delegate.collapseCircle()

    }

}

Это оказалось довольно огромным ответом, извините за это, я написал его немного поспешно, поэтому, если что-то неясно, просто скажите.

Надеюсь, это поможет!

Изменить: я нашел проблему, iOS 10 изменил способ отображения макетов, поэтому, чтобы исправить это, я добавил свойство endFrame в ResultViewController и установил его в виде view в viewDidLayoutSubviews. Я также устанавливаю как frame, так и endFrame одновременно в addCircle. Я изменил код выше, чтобы отразить изменения. Это не идеально, но я еще раз посмотрю, есть ли лучшее исправление.

Изменить: это то, что выглядит для меня открытым.

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

Ответ 2

Вы можете попробовать два разных вида контейнера для половины сверху и снизу. затем дать анимацию на нем...

Ответ 3

Спасибо всем за предложения, я попытался использовать представление контейнера, вот как я это сделал:

Сначала я добавил свойство containerView в класс CircularTransition:

class CircularTransition: NSObject {
    ...

    var containerView: UIView
    init(containerView: UIView) {
        self.containerView = containerView
    }

    ...    
}

Затем прокомментировал этот код в своем расширении:

// let containerView = transitionContext.containerView

// if UIDevice.current.userInterfaceIdiom == .pad {
//          viewCenter = CGPoint(x: viewCenter.x, y: viewSize.height)
//          viewSize = CGSize(width: viewSize.width, height: viewSize.height)
//      }

В моем mainViewController я добавил метод добавления вида контейнера:

func addContainerView() {

    let containerView = UIView()
    containerView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(containerView)
    NSLayoutConstraint.activate([
        containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
        containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
        containerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5),
        containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
        ])
    transtion.containerView = containerView
}

Причина, по которой я не использую панель рассказов, заключается в том, что если я помещаю анимированный контроллер представления (ResultViewController) в представление контейнера, он загружается всякий раз, когда загружается mainViewController, однако ResultViewController нужны данные из prepareForSegue, таким образом, он потерпит крах.

Затем я немного изменился в prepareForSegue:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    transtion.containerView = view
    if UIDevice.current.userInterfaceIdiom == .pad {
        addContainerView()
    }

    let secondVC = segue.destination as! ResultViewController
    secondVC.transitioningDelegate = self
    secondVC.modalPresentationStyle = .custom

    secondVC.dataSource = calculator!.iterateWPItems().0
}

И создал класс CircularTransition таким образом в mainViewController:

    let transtion = CircularTransition(containerView: UIView())

Что в основном все, что я сделал, я мог бы показать великолепный двойной просмотр vc на iPad, однако, переход возврата не работает, я все еще не выяснили, что вызвало это.

Ответ 4

Привет, я сделал некоторые изменения в вашем методе animateTransition, попробуйте это. Возможно, вам придется немного поиграть с функциейRelativeStartTime анимации, а также рамкой и центром, чтобы улучшить анимацию. Но я думаю, это должно заставить вас начать.

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    let containerView = transitionContext.containerView
    if transitionMode == .present {
        if let presentedView = transitionContext.view(forKey: UITransitionContextViewKey.to) {

            var viewCenter = presentedView.center
            var viewSize = presentedView.frame.size


            if UIDevice.current.userInterfaceIdiom == .pad {
                viewCenter = CGPoint(x: viewCenter.x, y: viewSize.height)
                viewSize = CGSize(width: viewSize.width, height: viewSize.height)
            }

            circle = UIView()
            circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
            circle.layer.cornerRadius = circle.frame.size.width / 2
            circle.center = startingPoint
            circle.backgroundColor = circleColor
            circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
            circle.layer.masksToBounds = true
            containerView.addSubview(circle)

            presentedView.center = startingPoint
            presentedView.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
            presentedView.alpha = 0
            containerView.addSubview(presentedView)

            UIView.animateKeyframes(withDuration: duration, delay: 0, options: .calculationModeLinear, animations: { 
                UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1, animations: { 
                    self.circle.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
                    presentedView.alpha = 1
                })
                UIView.addKeyframe(withRelativeStartTime: 0.19, relativeDuration: 1, animations: {
                    presentedView.transform = CGAffineTransform(scaleX: 1, y: 1)
                    presentedView.frame = CGRect(x: 0, y: (containerView.frame.size.height / 2)+10, width: containerView.frame.size.width, height: containerView.frame.size.height*0.5)
                })
                }, completion: { (sucess) in
                    transitionContext.completeTransition(sucess)
            })
        }
    } else {
        if let returningView = transitionContext.view(forKey: UITransitionContextViewKey.from) {
            let viewCenter = returningView.center
            let viewSize = returningView.frame.size

            circle.frame = frameForCircle(withViewCenter: viewCenter, size: viewSize, startPoint: startingPoint)
            circle.layer.cornerRadius = circle.frame.size.width / 2
            circle.center = startingPoint

            UIView.animate(withDuration: duration + 0.1, animations: {
                self.circle.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
                returningView.transform = CGAffineTransform(scaleX: 0.001, y: 0.001)
                returningView.center = self.startingPoint
                returningView.alpha = 0
                }, completion: {(success: Bool) in
                    returningView.center = viewCenter
                    returningView.removeFromSuperview()
                    self.circle.removeFromSuperview()
                    transitionContext.completeTransition(success)
            })
        }
    }
}

Надеюсь, что это поможет.