Since iOS8 we're allowed to use UISplitViewController on both compact and regular devices. This is great because I don't have to create two different storyboard for iPhone and iPad, but there's one problem that I'm stuck with.
If the split view controller is on iPad(if the collapsed property is NO), I can simply call this to show MasterVC on the left side.
self.splitViewController.preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay;
[self.splitViewController.displayModeButtonItem action];
But if it's on iPhone(if the collapsed property is YES), the displayMode is ignored, and doesn't do anything.
I cannot pop DetailVC with popToRootViewControllerAnimated because DetailVC has it's own navigation controller.
How does Apple expect us to show MasterVC(dismiss DetailVC) in code in collapsed mode if there isn't any method like dismissViewControllerAnimated:completion: for view controller that was presented with showDetail? Your help will be appreciated. Thanks
On devices which don't support the "split" mode, if
You want to present the master view controller instead of the detail when the UISplitViewController first loads, then returning YES in your delegate class (UISplitViewControllerDelegate) splitViewController:collapseSecondaryViewController:ontoPrimaryViewController: method method should do that:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController {
return YES;
}
You want to dismiss the detail view controller back to the master, after a specific event (e.g. a touch on a button). In this case you have to pop the detail view controller navigation controller:
[detailViewController.navigationController.navigationController popToRootViewControllerAnimated:YES]
Had a similar issue today trying to pop back from a detail view in a split view controller.
While I'm sure the accepted answer works fine, another approach I found that works as well and may be a bit cleaner is to use an unwind segue.
I setup an unwind segue on the master view I wanted to return to, then created a segue link to the unwind segue from the view I wanted to pop (note: assumes that you are using storyboards).
Make sure to setup the IBAction on the destination view you are popping back to:
-(IBAction)prepareForUnwind:(UIStoryboardSegue *)segue { }
Connect the exit to the segue in the storyboard for the unwind segue. Sorry, I'm not providing a lot of detail on how to setup the unwind segue, but there are many tutorials available for that.
Then on your controller you want to dismiss, connect a segue to the unwind segue of the controller you are popping back to. Be sure to name the segue.
Then on the button touch in the view controller you want to dismiss, just call
[self performSegueWithIdentifier:#"unwindSegueName" sender:self];
This worked really well and avoids digging backwards into a navigation hierarchy that may change.
Hope this is useful to someone!
Happy Holidays!
Here's what I ended up doing to pop the DetailVC if we are in a collapsed state (iPhone excluding +sizes), and show/hide the MasterVC if we are not in a collapsed state (iPad).
#IBAction func backTouchUp(_ sender: UIButton) {
if let splitViewController = splitViewController,
!splitViewController.isCollapsed {
UIApplication.shared.sendAction(splitViewController.displayModeButtonItem.action!, to: splitViewController.displayModeButtonItem.target, from: nil, for: nil)
} else {
navigationController?.popViewController(animated: true)
}
}
Thanks pNre! Here's code that will handle displaying a custom back button when collapsed and the displayModeButton when not collapsed.
lazy var backButtonItem: UIBarButtonItem = {
UIBarButtonItem(image: UIImage(named: "backImage"), style: .plain, target: self, action: #selector(dismissAnimated))
}()
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard let svc = splitViewController else { return }
if svc.isCollapsed {
navigationItem.leftBarButtonItem = backButtonItem
} else {
navigationItem.leftBarButtonItem = svc.displayModeButtonItem
}
}
func dismissAnimated() {
_ = navigationController?.navigationController?.popViewController(animated: true)
}
I've placed this in willLayoutSubviews() instead of viewDidLoad() so that the button will be updated adaptively, e.g., for orientation changes on iPhone 7 Plus and size class changes such as while in split view on iPad.
Related
I'm coding a simple app with swift and I'm stuck at the following point, I have two Controllers that lead to another one, and when I click on the cancel button, it always lead to the root Controller, no matter from where I come.
I have a first controller (UIViewController), that go to the Navigation Controller of my target Controller (the one from which I would like to go back to the right calling Controller).
I have a second controller (UITableViewController), which go directly to my target Controller.
Here's the code of my Cancel button:
// MARK: - Navigation
#IBAction func lendingCancelButton(_ sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways
let isPresentingInAddLendingMode = presentingViewController is UINavigationController
if isPresentingInAddLendingMode {
dismiss(animated: true, completion: nil)
} else if let owningNavigationController = navigationController {
owningNavigationController.popViewController(animated: true)
} else {
fatalError("the LendingViewController is not inside a navigation controller.")
}
}
If I correctly understood (you could then correct me if I'm wrong, I would learn something), it's testing if the ViewController that's presenting my target ViewController is a NavigationController.
So maybe that, as the second Controller (my UITableViewController) is not going through a NavigationController, so the last one calling my target view with a NavigationController is always the UIViewController.
Don't hesitate to tell me if it's not clear enough (too many times the word "Controller" in my post) or if you need additional code.
Try something like this
if let navigationController = presentingViewController as UINavigationController {
navigationController.popViewController(animated: true)
} else if let viewController = presentingViewController as UIViewController {
dismiss(animated: true, completion: nil)
} else {
fatalError("the LendingViewController is not inside a navigation controller.")
}
If i understood you want to use dismiss when you find a UIViewController and to pop the navigation when you find a UINavigationController right?
Ok so I finally found a way to make it working.
My tableViewController was embedded into a NavigationController. I removed it (since I could do without it, according to my need). From this View Controller, I draw a segue that "Show" my target view.
From my other ViewController (this one is embedded into a NavigationController), I draw a segue put that present modally my target view.
With the code provided in my initial post, it's working.
The only thing I didn't understand is why the NavigationController from my TableViewController was likely to cause it not working properly.
I use a push segue to transition from a uisearchcontroller located within my root view controller, to a second view controller. When I try to use an unwind segue method to transition back to the root view controller from my second view controller, my app does not transition unless the button connected to the unwind method is pressed twice. The unwind method is called both times, however the transition only occurs upon the second call. I do not know why this occurs. Any help is appreciated. Thanks!
Unwind segue method
#IBAction func saveWordAndDefinition(segue:UIStoryboardSegue) {
self.searchController.active = false
if let definitionViewController = segue.sourceViewController as? DefinitionViewController {
saveWordToCoreData(definitionViewController.word)
}
tableView.reloadData()
}
How I linked my segue
Unwind segue
While what you're doing is permissible, it seems to be against best practice. The functionality of presenting a view controller, UITableViewController in this case, entering information, then later dismissing it with a button in the upper-right hand corner is generally associated with a modal view. In a push segue you'll get the back button in the upper-left corner for free, which will enable to you to pop the view controller off the stack without writing extra code.
Here's another Stack Overflow question that describe: What is the difference between Modal and Push segue in Storyboards?
To answer your question specifically, here are a couple links that should help:
[self.navigationController popViewControllerAnimated:YES]; is probably what you're looking for.
Dismiss pushed view from within Navigation Controller
How can I dismiss a pushViewController in iPhone/iPad?
So here's how I finally got this to work:
In my FirstViewController (the vc i'm unwinding to):
Here is my unwind segue method.
#IBAction func saveWordAndDefinition(segue:UIStoryboardSegue) {
self.navigationController?.popViewControllerAnimated(false)
}
Then I gave my unwind segue the identifier "unwind" in Storyboard.
In my SecondViewController (the vc i'm unwinding from):
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "unwind" {
if let destination = segue.destinationViewController as? VocabListViewController {
destination.saveWordToCoreData(word)
destination.tableView.reloadData()
}
}
}
I took care of passing data in the prepareForSegue method of my SecondViewController. Thanks to #Lory Huz for the suggestion. I finally figured out what you meant by it.
Works without any errors!
I'm presenting a modal view controller using a custom transition (by setting its modelPresentationStyle to UIModalPresentationCustom, providing a transitioning delegate, and UIViewControllerAnimatedTransitioning object).
In the presented view controller, I have an unwind segue hooked up to a button. The segue fires just fine; the IBAction method in the presenting view controller is called, and so is prepareForSegue in the presented view controller. However, the presented view controller is not dismissed, and the appropriate transitioning delegate method (animationControllerForDismissedController:) is not called.
If, however, I set the presented view controller's modalPresentationStyle to UIModalPresentationFullScreen (the default), the view controller is dismissed properly (this breaks my custom transition, though).
I'm at a complete loss at what to do here. I've looked through Apple's documentation, and didn't notice anything saying that one had to do special things with unwind segues when dealing with custom transitions.
I'm aware that I could call dismissViewControllerAnimated:completion: in the IBAction method of the presenting view controller, but I'd rather use that as a last resort, and get the unwind segue working the way it should (or at least know why it's not working :) ).
Any help would be much appreciated,
Thanks in advance
It seems that if you use UIModalPresentationCustom to present the controller with a custom transition manager, you also need to use a custom transition manager to dismiss it (which makes sense I guess, since you can do all kinds of weird stuff in the animator object and UIKit can't be sure that just dismissing the screen as usual will completely restore the original state - I just wish it told you that explicitly...).
Here's what I've done to fix this in my app:
override segueForUnwindingToViewController in the parent view controller (the one to which you're moving after the dismiss animation) and return an instance of your UIStoryboardSegue, either the one you've used for the original transition or a new separate class
if the unwind segue's target view controller is in a navigation hierarchy, then you need to override that method in the navigation controller instead
in the perform method call dismissViewControllerAnimated
the presented view controller needs to still hold a valid reference to the transitioning delegate, or you'll get an EXC_BAD_ACCESS (see DismissViewControllerAnimated EXC_Bad_ACCESS on true) - so either make it keep the delegate as a strong reference as described in that thread, or assign a new one before calling dismissViewControllerAnimated (it's possible that changing modelPresentationStyle to e.g. full screen before dismissing would work too, but I haven't tried that)
if the dismiss animation needs to do any non-standard things (mine luckily didn't), override animationControllerForDismissedController in the transition manager object and return a proper animator
if the target view controller is in a navigation hierarchy, then you also need to manually pop the navigation stack to the target controller before dismissing the presented screen (i.e. target.navigationController!.popToViewController(target, animated: false))
Complete code sample:
// custom navigation controller
override func segueForUnwindingToViewController(toViewController: UIViewController,
fromViewController: UIViewController,
identifier: String?) -> UIStoryboardSegue {
return CustomSegue(
identifier: identifier,
source: fromViewController,
destination: toViewController
)
}
// presented VC
var customTransitionManager: UIViewControllerTransitioningDelegate?
// custom segue
override func perform() {
let source = sourceViewController as! UIViewController
if let target = destinationViewController as? PresentedViewController {
let transitionManager = TransitionManager()
target.modalPresentationStyle = .Custom
target.customTransitionManager = transitionManager
target.transitioningDelegate = transitionManager
source.presentViewController(target, animated: true, completion: nil)
} else if let target = destinationViewController as? WelcomeViewController {
target.navigationController!.popToViewController(target, animated: false)
target.dismissViewControllerAnimated(true, completion: nil)
} else {
NSLog("Error: segue executed with unexpected view controllers")
}
}
I also met this problem when I need to pass data back from the modalpresented view.
I wandering around Google and here for a couple of hours but I couldn't find an answer that is easy to understand for me. But I did get some hint and here's a work around.
It seems that because it has to pass data back, and the dismissing process from the automatic Unwind is prior before the data passing which prevented the ViewController being dismissed. So I think that I have to manually dismiss it once one more time.
I got some luck here. I didn't notice that it was a child viewcontroller. I just configured it from the storyboard.
And then in the Unwind function, I added to lines to remove the child viewcontroller and the child view. I have no code in the sourceViewController.
Swift 4.1
#IBAction func unwindToVC(sender :UIStoryboardSegue){
if let source = sender.source as? CoreLocationVC{
if source.pinnedCity != nil{
clCity = source.pinnedCity
}
if source.pinnedCountry != nil {
clCountry = source.pinnedCountry
}
if source.pinnedTimeZone != nil {
clTimeZone = source.pinnedTimeZone
}
if source.pinnedLocation != nil {
clLocation = source.pinnedLocation
}
// I added 2 lines here and it just worked
source.view.removeFromSuperview()
source.removeFromParentViewController()
}
Question
In a UISplitViewController collapsed display, how can I programmatically get back to master view controller?
Detail
I googled it but found no solution. Not sure if I was using the right keyword. This is how I show the detail view controller:
[self showDetailViewController:[[UINavigationController alloc] initWithRootViewController:detail] sender:self];
I also tried these 3 methods respectively, but none of them worked:
if (self.splitViewController.collapsed) {
UIBarButtonItem *backButtonItem = self.navigationItem.leftBarButtonItem;
(1):[backButtonItem.target performSelector:backButtonItem.action];
(2):[[UIApplication sharedApplication] sendAction:backButtonItem.action to:backButtonItem.target from:nil forEvent:nil];
(3):objc_msgSend(backButtonItem.target, backButtonItem.action);
}
navigation items set like thie in detail VC viewDidLoad:
self.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
self.navigationItem.leftItemsSupplementBackButton = YES;
Alright, I have found a solution that seems to work. I have tested it on iPhone 6 and iPhone 6 Plus, but I only just discovered it thirty minutes ago, so It might have some unfortunate side effect which I have not run into yet.
It's in swift. I hope it's clear though. Let me know if you need me to provide it in Objective-C instead.
if let splitViewController = splitViewController {
if splitViewController.collapsed {
let viewControllers = splitViewController.viewControllers
for controller in viewControllers {
// PrimaryNavigationController is the navigation controller I use
// as the split views master view, which is also set as its delegate
// but it could be any UINavigationController that is the
// primary controller of the split view
if controller.isKindOfClass(PrimaryNavigationController) {
controller.popViewControllerAnimated(true)
}
}
}
}
I call this from my detail view when I want to dismiss it.
The code works by checking if the split view controller is collapsed, which is the only state where popping the detail view makes sense (to me anyways). Then it simply looks for the navigation controller currently in play in the split view controller and asks it to pop it's top view controller. This works because when in collapsed mode, the split views master view is the only view controller in the stack. The detail view is collapsed "into" it, and therefore becomes the current top view controller of it, thus is the one that gets popped.
Seems to work. Let me know if it do for you too.
I was looking to do exactly the same, and this code worked for me. I put it in the detail view, hooked up to a button in the navigation bar.
In my application the detail view can segue to itself a number of times and this code gets one back to the master view no matter how deep down the line it gets.
#IBAction func unwindSegueId(sender: AnyObject) {
if (self.splitViewController!.collapsed) {
self.splitViewController!.viewControllers[0].popToRootViewControllerAnimated(true)
}
}
This seems to work (provided you have a navigation controller in your master pane)
if (self.splitViewController.collapsed) {
[(UINavigationController *)self.splitViewController.viewControllers[0]
popToRootViewControllerAnimated:YES];
}
I've got an app with a mapViewController embedded in a navController. In the mapVC ive got a single bar button item which when clicked I want to conditionally "push" segue to one of a number of different view controllers. To achieve that ive set up an ibaction on the button and have the conditional "performSegueWithIdentifier" code in the relevant buttons ibaction method ie
#IBAction func addButtonClicked(sender: UIBarButtonItem) {
let lastAdd = "addItem"
if lastAdd == "addItem"{
self.performSegueWithIdentifier("addItem", sender: self)
} else {
self.performSegueWithIdentifier("addEvent", sender: self)
}
}
this will take me to either the addItemVC or the addEventVC. in each of those viewControllers (ie the addItemVC and the addEventVC) I want to have a segmented control in the navigation bar which, when clicked, will take me to the alternative VC ie if addItemVC is currently displayed, and the addEvent section of the segmented control is clicked, I want to display the addEventVC. Im following Red Artisans page on how to do this but in his example he is instantiating all view controller options upfront in the app delegate and so can easily get reference to each view controller and link it to the clicked segement of the segmented control within his rootVC
Where im confused is .. seeing Im using conditional code before performing each segue, i assume that im only instantiating one viewController at a time when the bar button item is pressed. So how can i get an array of view controllers to pass to the VC im segueing to so that i can create the required segmented control in that VC. I assume i could manually create the destination VC array in my mapViewController and pass these across but wouldnt that mean im instatiating a different instance to the ones automatically created by the segue process?
Yes, you are right: if you manually create the two VCs in your mapViewController, they will be different instances from those created by a segue. So if you want to stick with Red Artisan's solution, present the VCs using code rather than segues. You can still design the two VCs in your storyboard, give them each a unique identifier and then use the instantiateViewControllerWithIdentifier function of self.storyboard to create the instances.
You can use most of Red Artisan's app delegate code in your mapViewController, but with a few tweaks: eg. to use the existing navigation controller (in which your mapViewController is embedded), and the [window ...] lines are superfluous. The thing to watch out for will be the indexDidChangeForSegmentedControl function, which assumes that the VCs you are switching between are the rootViewControllers for the navigation controller (ie. that they are the only item in the navigation controller's viewControllers array). In your case you have mapViewController as (I assume) the rootViewController, so you will have to amend the indexDidChangeForSegmentedControl function to create an array with the mapViewController at index 0 and the relevant (addItem or addEvent) VC at index 1. I don't know how well this method will animate, nor whether back buttons etc will be properly set.
If you want to stick with segues, there are a couple of solutions: one would be to use a UITabBarController (and hide the tabBar). You would have the addItem and addEvent VCs as separate tabs, and when you segue to the tabBarController, you could set which tab is selected. But my preferred solution would be to segue to a UIPageViewController. You would could either create the VCs in mapViewController and pass them as part of the segue, or just pass an indicator as to which was selected, and have the pageViewController instantiate them and present the relevant one. You could then use the UISegmentedControl to trigger switching between VCs. See this answer for something similar.
thanks pbasdf for your detailed instructions. its taken me quite a while but i seem to be close to getting it working. i followed most of your instructions and you were spot on with what you said.
first from the mapVC on press of the + bar button item i create the addItem and addEvent VCs using instantiateViewControllerWithIdentifier and create the segmented control.
#IBAction func addButtonClicked(sender: UIBarButtonItem) {
//at this stage just manually set default target VC
let lastAdd = "addItem"
//get an array of the target viewcontrollers
var viewControllers = segmentViewControllers()
//initz the segmentscontoller with the current navcontroller if doesnt already exist
if segmentsController == nil {
segmentsController = SegmentsController(navController: self.navigationController!, viewControllers: viewControllers)
segmentedControl.addTarget(segmentsController, action: "indexDidChangeForSegmentControl:", forControlEvents: UIControlEvents.ValueChanged)
//add the segmented control to the VC by setting first user experience which calls indexdidchangeforsegmentedcontrol
firstUserExperience()
}
segmentsController?.indexDidChangeForSegmentControl(segmentedControl)
}
//create an array of the target view controller. called from addbutton clicked
func segmentViewControllers() -> [UIViewController] {
//create an instance of the viewcontrollers
let addItemVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("AddItemVC") as ViewController
let addEventVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("AddEventVC") as ViewController
var viewControllers = [addItemVC, addEventVC]
return viewControllers
}
in the segmentsController i created 2 arrays, 1 to hold the different VCs and one to hold the navigation stack. i also set the passed the segmented control to the incoming VCs
class SegmentsController: NSObject {
var navController: UINavigationController?
var viewControllersOptionsArray: [UIViewController] = []
var viewControllersNavArray: [UIViewController] = []
//MARK: - INITIALIZER
init(navController: UINavigationController, viewControllers: [UIViewController]) {
self.navController = navController
self.viewControllersOptionsArray = viewControllers
}
//MARK: SEGMENT INDEX METHOD
func indexDidChangeForSegmentControl(segmentedControl: UISegmentedControl) {
var index = segmentedControl.selectedSegmentIndex
var incomingViewController = viewControllersOptionsArray[index]
//set the viewControllersNavArray
if let mapVC = navController?.viewControllers[0] as? MapViewController {
viewControllersNavArray = [mapVC, incomingViewController]
}
//set the navcontroller with a new array of viewcontrollers
navController?.setViewControllers(viewControllersNavArray, animated: true)
//set the title of the incoming view controller
incomingViewController.navigationItem.titleView = segmentedControl
//set the seg control variable of the incoming VCs
if let iVC = incomingViewController as? AddItemViewController {
iVC.segmentedControl = segmentedControl
} else if let iVC = incomingViewController as? AddEventViewController {
iVC.segmentedControl = segmentedControl
}
}
}
In the addItem and addEvent VCs i figured i needed to pass the current selectedSegmentIndex back to the mapVC if the user presses the back button - wasnt sure how to do this and ended up using an extension i downloaded called UIViewController+BackButtonHandler to handle it.
Im sure my code could be much better written but the only thing that im still having trouble with is that i want the VC transitions to be animated. the navigation seems to work fine if i set animated to false in the navController?.setViewControllers(viewControllersNavArray, animated: true) line but if i set it to true, the segmentedControl briefly flashes an appearance on the nav bar of the incoming VC but then disappears. Its still there so i can still navigate but you cant see it. i figure that it has something to do with setting it before the view has properly loaded but even if i put the code to set it ie
incomingViewController.navigationItem.titleView = segmentedControl
in the new top VC in viewDidLoad or viewDidAppear i still get the same problem. i also thought i might be able to fix it if i could put the incomingViewController.navigationItem.titleView = segmentedControl code in a completion block but the setViewControllers method doesnt appear to have a completion handler.
Any suggestions?