How can I push a UIViewController (VC) presenting a modal view (MV) to screen with a single left-to-right transition animation?
I have tried:
Setting the modal transition style of MV, and then adding both controllers to the viewControllers of the navigation controller. This however results in MV just being a controller dismissed as any other controller in the stack.
I have tried presenting MV from VC with no animation, then adding VC to the viewControllers and presenting the stack as above. This resulted in MV being instantly presented when the transition to VC starts, while VC itself animates to screen as expected. E.g. the MV doesn't follow VC when it slides in.
I have tried presenting MV from VC with animation enabled, but that results in two transitions: first VC animates to screen, then VC slides up.
I'm out of ideas, yet I would like a native and clean solution: How to transition to VC with a single normal left-to-right push, when VC is entirely covered by MV?
Key point is that MV animates together with VC; MV would appear and behave like a full screen subview, but when dismissed it animates off screen like any other modal view controller.
There are many solutions to this issue. I think UIPresentationController can provide an important clue here.
I try to make the answer as simple as possible here. You may change parameters or even subclass UIPresentationController to achieve a full animation as you wish.
import UIKit
//Green
class TransViewController: UIViewController {
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
let dest = segue.destination
let nextVC = storyboard?.instantiateViewController(withIdentifier: "modelViewController")
if let myPresenter = dest.presentationController{
myPresenter.presentedView!.addSubview(nextVC!.view)
nextVC!.view.center = CGPoint.init(x: nextVC!.view.center.x + nextVC!.view.frame.width , y: nextVC!.view.center.y)
UIView.animate(withDuration: 0.5, animations: {
nextVC!.view.center = self.view.center
}) { (success) in
dest.present(nextVC!, animated: false, completion: nil)
}
}
}
}
//Yellow ; "modelViewController"
class ModelViewController: UIViewController {
#IBAction func click(_ sender : UIButton){
self.dismiss(animated: true, completion: nil)
}
}
Related
I have a popover view which is simply a stack of UIButtons.
The popover is presented from a view controller (Records) which is itself inside a NavigationController.
I need the buttons in popover view to be able to push other views on top of the navigation stack.
Here's how I prepare the segue for the popover in the Records view controller:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "popoverSegue" {
let dest = segue.destination as! PopoverViewController
dest.navController = navigationController
dest.modalPresentationStyle = .popover
dest.popoverPresentationController?.barButtonItem = addButton
dest.popoverPresentationController?.delegate = self
}
}
Then inside the popoverViewController I got a bunch of IBAction functions where I need to push other views on top of the navController that was set above.
let editor = EditorViewController(nibName: "EditorViewController", bundle: nil)
navController?.pushViewController(editor, animated: true)
This kina works and the editor view shows up with a nav bar and all, but as soon as I tap on the view or try to scroll, it just gets dismissed.
How can I prevent that dismiss thing? I did try setting isModalInPresentation. It didn't work for me.
Answering, as per OP's comments...
The proper approach is to have your "popover" controller tell the presenting controller to push a new VC onto the navigation stack.
This can be done in a few different ways, but most commonly by using either the protocol/delegate pattern or with closures.
I have a UILabel in my ViewController that has a NavigationController (let's say view controller A) with a tap gesture recognizer attached to the label. When the label is tapped another view appears (let's call it B). The user picks some text in B and the view dismisses back to A with the label text updated with the selection. So I created a delegation between A and B to get the selection. The problem is that I do not see the NavigationBar when B appears. Is there a way to fix this?
ViewController A
#IBOutlet weak var sectionName: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let sectionLabelTap = UITapGestureRecognizer(target: self, action: #selector(labelTapped(_:)))
sectionName.isUserInteractionEnabled = true
sectionName.addGestureRecognizer(sectionLabelTap)
}
#objc func labelTapped(_ sender: UITapGestureRecognizer) {
let sectionNameVC = storyboard?.instantiateViewController(withIdentifier: "SectionName") as! SectionNameTableViewController
sectionNameVC.selectionNameDelegate = self
sectionNameVC.userData = userData
present(sectionNameVC, animated: true, completion: nil)
}
In order to display the Navigation bar the UIViewController needs to have a UINavigationController.
You can add that sectionNameVC ViewController into a UINavigationController to persevere the present animation.
In that case your code might look something like this:
#objc func labelTapped(_ sender: UITapGestureRecognizer) {
let sectionNameVC = storyboard?.instantiateViewController(withIdentifier: "SectionName") as! SectionNameTableViewController
sectionNameVC.selectionNameDelegate = self
sectionNameVC.userData = userData
let naviagtionController = UINavigationController(rootViewController: sectionNameVC)
present(naviagtionController, animated: true, completion: nil)
}
Or you can simply call pushViewController on the View Controller A's navigation Controller, like this:
self.navigationController?.pushViewController(sectionNameVC, animated: true)
This will add sectionNameVC into the View Controller A's navigation Controller stack. In this case the transition animation will be different, the sectionNameVC will come from your right.
You are missing the concept between "Presenting" View Controller & "Navigating" the View Controller. You will get the answer, once you understood the concept. Here, it is..
When you are presenting the ViewController, you are completely replacing the stack container to the new view controller.
STACK holds the addresses of the ViewControllers you push or pop via navigating.
e.g:
present(sectionNameVC, animated: true, completion: nil)
On the other hand, if you are navigating to other view controller by pushing it. In this case, you can go back to previous controller by simple popping the ViewController address from stack.
e.g:
self.navigationController?.pushViewController(sectionNameVC, animated: true)
self.navigationController?.popViewController(animated: true)
So, If you navigate then, only you will get navigation Bar.
Now, in your case, you are presenting the ViewController and hence, navigation bar is not showing.
My goal is to perform a flip-animation from view1 to view2 and vice versa.
I only want to flip the views not the whole viewController
Set up as follows:
i have a view controller (fromViewController) in my storyboard embedded in a navigationController. I drew a segue to a second view controller (toViewController) and made it a custom segue.
Now i want to add a flip animation to flip the views of the view controllers from left to right and right to left.
The forward animation already works (from fromViewController to toViewController) but he backward animation does not work.
I checked a lot of similar questions here but i couldn't find an answer that fit my needs.
I hope someone can help me out of this.
The class for the custom segue is FlipSegue.
here is my code:
import UIKit
extension UIViewController{
var isVisible: Bool{
return self.isViewLoaded && self.view.window != nil
}
}
class FlipSegue: UIStoryboardSegue {
override func perform() {
let fromViewController = self.source
let toViewController = self.destination
if fromViewController.isVisible{
UIView.transition(from: fromViewController.view, to: toViewController.view, duration: 1, options: .transitionFlipFromLeft, completion: nil)
}
else{
UIView.transition(from: toViewController.view, to: fromViewController.view, duration: 1, options: .transitionFlipFromRight, completion: nil)
}
}
}
FYI: The toViewController is not part of the NavigationController.
EDIT: I perform the segue by tapping on a UIBarButton on the left side of the navigation controller. The Button is calling the perform function in the FlipSegue class. Maybe the problem is that the toViewController is not part of the navigation stack? So i added it to the navigation stack and then i got a back button and i could return to the fromViewController with the standard animation, but this is not what i wanted.
I haven't tried this, but you should probably use from:fromViewcontroller.view in both cases, since in a back segue, from is still the starting VC.
I am creating an application which shows a PageVC: UIPageViewController as intro and guide to app.
After navigating through the intro, a Next Button leads to "SettingsVC" which is used to store default settings for the app.
Settings have to be chosen by the user initially although can be changed later.
PageVC ---> SettingsVC
A Save and a Cancel button on the SettingsVC leads to the MainVC of the app.
A button on MainVC leads to SettingsVC.
SettingsVC <---> MainVC
The app would work as follows:
if isFirstLaunch{
instantiate PageVC
}else{
instantiate MainVC
}
in PageVC
nextButtonPressedinPageVC{
instantiate SettingsVC
}
in SettingsVC
if saveButtonPressed && cameFromPageVC{
instantiate MainVC
}
if cancelButtonPressed && cameFromPageVC {
do Nothing
}
if saveButtonPressed && cameFromMainVC{
dismiss currentVC
}
if cancelButtonPressed && cameFromMainVC {
dismiss currentVC
}
in MainVC
if settingsButtonPressedinMainVC {
instantiate SettingsVC
}
I have made sure that if it is application's first launch, PageVC will be instantiated else MainVC will be instantiated.
How can I move between the viewControllers without a possible memory leak i.e. where to performSegue and where to dismiss current VC?
Please include code for reference.
There are many ways to do this, here is one that I find very straightforward because you can do most of the work in your Storyboard:
Think of your MainVC as the rootViewController and the other two as accessory views that will only temporarily be shown. The MainVC should always be your entry point, so set it as the initial VC in your Storyboard.
The other two should be displayed modally so that you can easily return to the MainVC by dismissing them, no matter how you opened them in the first place.
To do this, draw a segue from your MainVC button to the PageVC and name it "showPageVC". From the Next button in your PageVC, draw another segue to the SettingsVC. Now you need some code to handle the dismiss actions: put this snippet in your MainVC:
#IBAction func unwindToMain(segue:UIStoryboardSegue) {
}
This function is just a marker, so it doesn't need a body. It just enables you to create a unwind segue back to MainVC: For each of the buttons in SettingsVC, hold Ctrl and draw from the button to the right exit icon in the header of the SettingsVC storyboard scene and choose unwindToMain in the tiny black popup.
Now you only have to implement the logic to decide if you want to show the PageVC or not in viewDidAppear() of the MainVC. So the whole code would look something like this:
class MainVC: UIViewController {
var didDisplayPageVC = false
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if (didDisplayPageVC == false) {
performSegue(withIdentifier: "showPageVC", sender: self)
didDisplayPageVC = true
}
}
#IBAction func unwindToMain(segue:UIStoryboardSegue) {
}
}
The rest is in the storyboard. If this little proof-of-concept is working, you can go and configure the segues (you might want to remove the animation) etc.
I've looked around for an answer for this and spent the last two hours pulling my hair out to no end.
I'm implementing a very basic custom view controller transition animation, which simply zooms in on the presenting view controller and grows in the presented view controller. It adds a fade effect (0 to 1 alpha and visa versa).
It works fine when presenting the view controller, however when dismissing, it brings the presenting view controller back in all the way to fill the screen, but then it inexplicably disappears. I'm not doing anything after these animations to alter the alpha or the hidden values, it's pretty much a fresh project. I've been developing iOS applications for 3 years so I suspect this may be a bug, unless someone can find out where I'm going wrong.
class FadeAndGrowAnimationController : NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
func animationControllerForPresentedController(presented: UIViewController!, presentingController presenting: UIViewController!, sourceController source: UIViewController!) -> UIViewControllerAnimatedTransitioning! {
return self
}
func animationControllerForDismissedController(dismissed: UIViewController!) -> UIViewControllerAnimatedTransitioning! {
return self
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning!) -> NSTimeInterval {
return 2
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning!) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as UIViewController
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as UIViewController
toViewController.view.transform = CGAffineTransformMakeScale(0.5, 0.5)
toViewController.view.alpha = 0
transitionContext.containerView().addSubview(fromViewController.view)
transitionContext.containerView().addSubview(toViewController.view)
transitionContext.containerView().bringSubviewToFront(toViewController.view)
UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: {
fromViewController.view.transform = CGAffineTransformScale(fromViewController.view.transform, 2, 2)
fromViewController.view.alpha = 1
toViewController.view.transform = CGAffineTransformMakeScale(1, 1)
toViewController.view.alpha = 1
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
})
}
}
And the code to present:
let targetViewController = self.storyboard.instantiateViewControllerWithIdentifier("Level1ViewController") as Level1ViewController
let td = FadeAndGrowAnimationController()
targetViewController.transitioningDelegate = td
targetViewController.modalPresentationStyle = .Custom
self.presentViewController(targetViewController, animated: true, completion: nil)
As you can see, a fairly basic animation. Am I missing something here? Like I said, it presents perfectly fine, then dismisses 99.99% perfectly fine, yet the view controller underneath after the dismissal is inexplicably removed. The iPad shows a blank screen - totally black - after this happens.
This seems to be an iOS8 bug. I found a solution but it is ghetto. After the transition when a view should be on-screen but isn't, it needs to be added back to the window like this:
BOOL canceled = [transitionContext transitionWasCancelled];
[transitionContext completeTransition:!canceled];
if (!canceled)
{
[[UIApplication sharedApplication].keyWindow addSubview: toViewController.view];
}
You might need to play around with which view you add back to the window, whether to do it in canceled or !canceled, and perhaps making sure to only do it on dismissal and not presentation.
Sources: Container view disappearing on completeTransition:
http://joystate.wordpress.com/2014/09/02/ios8-and-custom-uiviewcontrollers-transitions/
I was having the same problem when dismissing a content view controller. My app has this parent view controller showing a child view controller. then when a subview in the child is tapped, it shows another vc (which I am calling the content vc)
My problem is that, when dismissing the contentVC, it should go to child VC but as soon as my custom transition finishes, childVC suddenly disappears, showing the parent VC instead.
What I did to solve this issue is to
change the .modalPresentationStyle of the childVC presented by parentVC from the default .automatic to .fullscreen.
Then changed the .modalPresentationStyle of contentVC to .fullscreen as well.
This solves the issue. but it won't show your child VC as a card sheet (when using .overCurrentContext or automatic) which is new in iOS 13.
had the same problem ios 8.1
my swift code after completeTransition
if let window = UIApplication.sharedApplication().keyWindow {
if let viewController = window.rootViewController {
window.addSubview(viewController.view)
}
}