I'm aware that you can switch from one ViewController to another using an intermediary Class, as seen in this example.
What I want to know is, is there any way to switch to a different view controller directly from another view controller? Like, load the view in the current view controller, and release the one you're in immediately after?
Thanks.
I think what you are asking for is how to retain 1 view controller.
What you can do is when you add your new View Controller, add it to your parent view controller and remove the current view controller.
Using UINavigationController is one way: [navigationController pushViewController:animated:]. The other "official" way is to display the next view as a modal view: [someVC presentModalViewController:], but this is deprecated since iOS 6.
The iOS 6 way is: - (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion.
var currentVC:UIViewController!
let firstVC = yourViewcontroller1()
let secondVC = yourViewcontroller2()
func setupChildViewControllers(){
self.addChildViewController(firstVC)
self.addChildViewController(secondVC)
self.view.addSubview(firstVC.view)
self.currentVC = firstVC
}
func replaceController(oldVC:UIViewController,newVC:UIViewController){
self.addChildViewController(newVC)
self.transitionFromViewController(oldVC, toViewController: newVC, duration: 0, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: nil) { (finished) -> Void in
if finished {
newVC.didMoveToParentViewController(self)
oldVC.willMoveToParentViewController(nil)
oldVC.removeFromParentViewController()
self.currentVC = newVC
}else{
self.currentVC = oldVC
}
}
}
Related
My app has a TabBarController. Each tabBarItem relates to a ViewController embedded in a NavigationController.
When in first tabBarItem and selecting another tabBarItem I want to do some stuff before moving to the selected ViewController. Therefore I created a class for my tabBarController and made it UITabBarControllerDelegate.
What I want to do is present an alert with two buttons; button A cancels the move to the selected viewController and button B lets the move happen.
My problem is that when button B is pressed, I want to popToRootViewController. I gave the navigationController a storyboardID and tried to instantiate it like shown below.
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if let alert = self.storyboard?.instantiateViewController(withIdentifier: "ActiveSessionWarningAlert") as? ActiveSessionWarningAlert {
alert.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
alert.modalTransitionStyle = UIModalTransitionStyle.flipHorizontal
let alertWasDismissed: (Bool) -> Void = { userWantsToMoveToSelectedViewController in
if userWantsToMoveToSelectedViewController {
if let navContr = self.storyboard?.instantiateViewController(withIdentifier: "firstNavContr") as? UINavigationController {
navContr.popToRootViewController(animated: true)
}
tabBarController.selectedViewController = viewController
}
}
alert.alertWasDismissed = alertWasDismissed
self.present(alert, animated: true, completion: nil)
}
return false
}
Everything works as expected, but the popToRootViewController doesn't seem to occur; when selecting the first tabBarItem again the same viewController that was 'active' when we left the item is still showing.
I checked so that the viewController I want to pop is actually in the navigation stack and that the navContr != nil.
What am I missing?
You don't say so, but I'm assuming that the alertWasDismissed closure you pass to your alert view controller gets invoked when the user dismisses the alert.
The problem with your closure is this bit:
if let navContr = self.storyboard?.instantiateViewController(withIdentifier: "firstNavContr") as? UINavigationController
Any time you call instantiateViewController(withIdentifier:), you are creating a brand new, never before seen instance of a view controller (a navigation controller in this case.) That navigation controller has nothing to do with the one that belongs to the current tab that you are trying to dismiss. It doesn't have anything in it's navigation stack other than the root view controller that is defined in the storyboard.
What you need to do is find the navigation controller of the current tab in your tab bar controller's tabBarController(_:shouldSelect:) method, and pass that navigation controller to your alertWasDismissed closure.
At the time the tabBarController(_:shouldSelect:) method is called, the tab bar controller's selectedViewController should contain the current view controller. Replace the line above with:
if let navContr = tabBarController.selectedViewController? as? UINavigationController {}
That should work.
I have the next function
func switchRootViewController(rootViewController: UIViewController) {
let window = UIApplication.shared.windows.first!
window.rootViewController = rootViewController
window.makeKeyAndVisible()
}
And I use it in a presented view controller, ex in PresentedViewController.
let navigationViewController = UINavigationController(rootViewController: PresentedViewController())
present(navigationViewController, animated: true, completion: nil)
But when I switch to the needed view controller, my presented view controller doesn't deinitialize. I have to use this way, first dismiss:
self.dismiss(animated: true, completion: {
switchRootViewController(rootViewController: HomeViewController.instantiate())
})
instead of simple
switchRootViewController(rootViewController: HomeViewController.instantiate())
UIViewController objects are retained when presented by another UIViewController. To release your view controllers, they need to be removed from the view hierarchy, which you can accomplish by dismissing them. As far as where to dismiss your view controller, I'm not sure because you haven't given enough information. But you need to dismiss any view controller that you want released from memory.
I Want to add a view Controller on another View Controller for 5 Second?
May Help me as i'[m new for swift programming.
Thanks in advance.
You should use this to make the screen disappear after 5s:
let time = dispatch_time(dispatch_time_t(DISPATCH_TIME_NOW), 5 * Int64(NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue()) {
//put your code to remove the screen
}
Call another view controller and then on second view controller add 5 second sleep on viewDidAppear function and then again call first viewController.
override func viewDidAppear(animated: Bool)
{
print("Inside View Did Appear")
sleep(5)
print("After 5 second")
let initialViewController = self.storyboard!.instantiateViewControllerWithIdentifier("RevealViewController")
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window?.rootViewController = initialViewController
appDelegate.window?.makeKeyAndVisible()
}
there is too many ways to do it.For my advice go with storyboard.If subviewcontroller only small part of parent controller you better to use container view controller in storyboard.If gonna use it like a overlay connect two controller using segue with "Present Modally" kind.You can find samples in google if you search for container view controller or segue connection.
For dismissing child controller if you choose segue way you can schedule self.dismissViewControllerAnimated(false, completion: nil) code in second controller inside the didViewAppear method.For container view case you can write dismiss code in firstview controller and remove container from superview.
I am trying to use a custom segue, that makes the view 'scroll' to the right or left, when a button is clicked. I added a custom class that looks like this
class horizontalSegue : UIStoryboardSegue {
override func perform() {
var oldView = self.sourceViewController.view as UIView
var newView = self.destinationViewController.view as UIView
oldView.window?.insertSubview(newView, aboveSubview: oldView)
newView.center.x = oldView.center.x + oldView.frame.width
newView.center.y = oldView.center.y
UIView.animateWithDuration(0.6, animations: { newView.center = oldView.center }, completion: { finished in Void })
}
}
but the problem is that after I segue once, I cannot segue back to the view I segued from. I think its because of the way I have the oldView and newView declared, the app isn't updating which view is which, so its segueing back to the current view when I try to segue to the first view.
If I am correct, how would I make sure the app updates which view is which?
Apple documentation says: "Regardless of how you perform the animation, at the end of it, you are responsible for installing the destination view controller (and its views) in the right place so that it can handle events. For example, if you were to implement a custom modal transition, you might perform your animations using snapshot images and then at the end call the presentModalViewController:animated: method (with animations disabled) to set up the appropriate modal relationship between the source and destination view controllers."
May be you should add something like
[self.navigationController pushViewController:vc animated:NO]
into your animation's completion handler?
I'm having a UISplitViewController that contains a UITabBarController as master view.
This UITabBarController contains a UINavigationController. The detail view contains a UINavigationController as well.
On the iPad this works as expected. The show detail segue presents the imageview within the navigation controller on the detail view.
On the iPhone on the other hand I expected that the show detail segue pushes the detail view on the stack of the navigation controller of the master view. But actually it is presented modally over the master view.
When removing the UITabBarController from the storyboard and using the UINavigationController directly in the master view this works.
Has anybody an idea how I could present the detail view on the stack of the master's UINavigationController on an iPhone?
The problem with Peter's solution is that it will fall apart with the iPhone 6 +. How so? With that code, if an iPhone 6 + is in portrait orientation - the detail view pushes onto the navigation stack. All is well, so far. Now, rotate into landscape, and then you'll have the detail view showing as the detail view and the master view.
You'll need the split view controller's delegate to implement two methods:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)detailVC sender:(id)sender
{
UITabBarController *masterVC = splitViewController.viewControllers[0];
if (splitViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact)
[masterVC.selectedViewController showViewController:detailVC sender:sender];
else
[splitViewController setViewControllers:#[masterVC, detailVC]];
return YES;
}
And now, you'll need to return the top view controller from the selected tab's navigation controller:
- (UIViewController*)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController
{
UITabBarController *masterVC = splitViewController.viewControllers[0];
if ([(UINavigationController*)masterVC.selectedViewController viewControllers].count > 1)
return [(UINavigationController*)masterVC.selectedViewController popViewControllerAnimated:NO];
else
return nil; // Use the default implementation
}
With this solution, everything pushes onto the navigation stack when it should and also updates the detail view correctly on the iPad/6+ landscape.
I figured out how to put the detail on to the master's UINavigationController instead of presenting it modally over the UITabBarController.
Using the UISplitViewControllerDelegate method
- splitViewController:showDetailViewController:sender:
In case the UISplitViewController is collapsed get the masters navigation controller and push the detail view onto this navigation controller:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController
showDetailViewController:(UIViewController *)vc
sender:(id)sender {
NSLog(#"UISplitViewController collapsed: %d", splitViewController.collapsed);
// TODO: add introspection
if (splitViewController.collapsed) {
UITabBarController *master = (UITabBarController *) splitViewController.viewControllers[0];
UINavigationController *masterNavigationController = (UINavigationController *)master.selectedViewController;
// push detail view on the navigation controller
//[masterNavigationController pushViewController:vc animated:YES];
// push was not always working (see discussion in answer below), use showViewController instead
[masterNavigationController showViewController:vc sender:sender];
return YES;
}
return NO;
}
The answer of #PeterOettl to his own question put me on the right way and is great for that. So the credit belongs to him.
I have nearly the same storyboard structure as him, but as vc is a navigationController I get a runtime error saying
'Pushing a navigation controller is not supported'
As said, that is because vc is the navigationController of the detail view and not the viewController of the detail view.
Note that I am surprised that #PeterOettl does not get that error in his case also, as the segue given in the storyboard picture, points to the navigation controller of the detail view.
Therefore the code should like that (in Swift) simply adding
let detailViewControllerNavigationController = (vc as UINavigationController).viewControllers[0] as UIViewController
and pushing detailViewControllerNavigationController instead of vc
and the whole code is
func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
println("UISplitViewController collapsed: \(splitViewController.collapsed)")
if (splitViewController.collapsed) {
let master = splitViewController.viewControllers[0] as UITabBarController
let masterNavigationController = master.selectedViewController as UINavigationController
let detailViewControllerNavigationController = (vc as UINavigationController).viewControllers[0] as UIViewController
masterNavigationController.pushViewController(detailViewControllerNavigationController, animated: true)
return true
} else {
return false
}
}
Also note that this code is put in the AppDelegate.swift of the master-detail example of Xcode where a tab bar is added in the master view.
EDIT
In the comments we discussed with #PeterOettl of the difference between .pushViewController and .showViewController.
Apple documentation says :
showViewController:sender:
This method pushes a new view controller
onto the navigation stack in a similar way as the
pushViewController:animated: method. You can call this method directly
if you want but typically this method is called from elsewhere in the
view controller hierarchy when a new view controller needs to be
shown.
Available in iOS 8.0 and later.
I appreciate this discussion thread when I was implementing exactly the same UI structure app, and furthurmore made it adaptive for iPhone 6 Plus rotation and iPad multitasking (Slide Over/Split View, iOS 9 or later).
We have put the full solution (adaptive UISplitViewController with UITabBarController as primary view controller) open sourced on GitHub indievox-inc/TabBarSplitViewController. Thanks!
I implemented #Dreaming In Binary's answer in Swift:
func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
let masterVC = splitViewController.viewControllers[0] as UITabBarController
if splitViewController.traitCollection.horizontalSizeClass == .Compact {
masterVC.selectedViewController?.showViewController(vc, sender: sender)
} else {
splitViewController.viewControllers = [masterVC, vc]
}
return true
}
func splitViewController(splitViewController: UISplitViewController, separateSecondaryViewControllerFromPrimaryViewController primaryViewController: UIViewController!) -> UIViewController? {
let masterVC = splitViewController.viewControllers[0] as UITabBarController
if let navController = masterVC.selectedViewController as? UINavigationController {
if navController.viewControllers.count > 1 {
return navController.popViewControllerAnimated(false)
}
}
return nil
}