Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Display 2 view controllers at the same time with animation

I'm following this awesome video to create a custom transition for my project, because I'm developing for the iPad, so instead of presenting destination view controller full screen, I want to have it occupy half of the screen like this:

enter image description here

My code of the custom transition class is:

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)

}
}

And the part of code in my view controller:

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
}

But the controller shows up full screen.

like image 285
Bright Avatar asked Oct 10 '16 07:10

Bright


4 Answers

You may try the two different Container View for half of top and bottom. then give animation on it...

like image 178
MiTal Khandhar Avatar answered Nov 19 '22 02:11

MiTal Khandhar


So I have finished creating my answer, It takes a different approach than the other answers so bear with me.

Instead of adding a container view what I figured would be the best way was to create a UIViewController subclass (which I called CircleDisplayViewController). Then all your VCs that need to have this functionality could inherit from it (rather than from UIViewController).

This way all your logic for presenting and dismissing ResultViewController is handled in one place and can be used anywhere in your app.

The way your VCs can use it is like so:

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()

    }

}

Where showCircle will present your ResultViewController using the transitioning delegate with the circle center at the sending UIButtons center.

The CircleDisplayViewController subclass is this:

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
    }

}

The only function that is called by the VCs that inherit from DisplayCircleViewController is openCircle, openCircle has a circleCenter argument (which should be your button center I'm guessing), an optional radius argument (if this is nil then a default value of half the view height is taken, and then whatever else you need to setup ResultViewController.

In the addCircle func there is some important stuff:

you setup ResultViewController however you have to before presenting (like you would in prepare for segue),

then setup the frame for it (I tried to make it the area of the circle that is visible but it is quite rough here, might be worth playing around with),

then this is where I reset the transition circle (rather than in the transition class), so that I could set the circle starting point, radius and frame here.

then just a normal present.

If you haven't set an identifier for ResultViewController you need to for this (see the UIStoryboard extensions)

I also changed the TransitioningDelegate functions so you don't set the circle center, this is because to keep it generic I put that responsibility to the ViewController that inherits from this one. (see top bit of code)

Finally I changed the CircularTransition class

I added a variable:

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

and changed animateTransition:

(removed the commented out lines):

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)

            ...

        }
    }
}

Finally I made a protocol so that ResultViewController could dismiss the circle

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's 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()

    }

}

This has turned out to be quite a huge answer, sorry about that, I wrote it in a bit a of rush so if anything is not clear just say.

Hope this helps!

Edit: I found the problem, iOS 10 has changed the way they layout views, so to fix this I added an endFrame property to ResultViewController and set it's views frame to that in viewDidLayoutSubviews. I also set both the frame and endFrame at the same time in addCircle. I changed the code above to reflect the changes. It's not ideal but I'll have another look later to see if there is a better fix.

Edit: this is what it looks like open for me

enter image description here

like image 28
Olivier Wilkinson Avatar answered Nov 19 '22 00:11

Olivier Wilkinson


Thanks to everyone for the suggestions, I tried to use a container view, here's how I did it:

First I added a containerView property in CircularTransition class:

class CircularTransition: NSObject {
    ...

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

    ...    
}

Then commented out these code in its extension:

// 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)
//      }

In my mainViewController, I added a method to add a container view:

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
}

The reason I don't use story board is, if I put the animated view controller (ResultViewController) in the container view, it gets loaded whenever mainViewController is loaded, however, ResultViewController needs the data from prepareForSegue, thus it'll crash.

Then I changed a little bit in 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
}

And created CircularTransition class this way in mainViewController:

    let transtion = CircularTransition(containerView: UIView())

That's basically all I did, I could display the gorgeous dual vc view on the iPad, however, the return transition doesn't work, I still haven't figured out what caused that.

like image 2
Bright Avatar answered Nov 19 '22 00:11

Bright


Hi i did some changes in your animateTransition method try this out. You might have to play a little bit with withRelativeStartTime of the animations and the frame and center to perfect the animation. But i guess this should get you started.

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)
            })
        }
    }
}

Hope this helps.

like image 1
nishith Singh Avatar answered Nov 19 '22 01:11

nishith Singh