Strange things seem to happen when using the new iOS 11 navigationItem.searchController method on a detail view of a UISplitViewController.
The searchBar partly appears as a blank space on the first presentation, then appears in the wrong UITableViewController, but corrects itself after a few push and pops of UITableViewController.
I used to put the searchBar in the tableHeaderView, but I changed the code according to the WWDC recommendation:
if (#available(iOS 11.0, *)) {
self.navigationItem.searchController = self.searchController;
self.navigationItem.hidesSearchBarWhenScrolling = NO;
} else {
self.tableView.tableHeaderView = self.searchController.searchBar;
}
This example is using standard sample code (default project for UISplitViewController and the Apple demo of UISearchController updated for iOS 11 (using a single UITableViewController)).
The initial view containing the searchController looks like this:
And clicking a UITableView item yields this:
However after clicking on a UITableView item and returning twice - it looks as it should:
and:
I was trying to determine why the Apple example for UISearchController worked and my code didn't. The main difference was it was embedded in UISplitViewController in the Detail View. Which means if shown in Compact mode has an extra UINavigationController in the stack. I found if my seque avoided the extra UINavigationController - it works correctly (but breaks device rotation). Similarly change the segue to modal allows it to work.
I note this is similar to this old question: UISplitViewController with new UISearchController issue with UISearchBar
I have created a sample project that demonstrates the problem (sample code: searchControllerDemo)
I'm stumped as to what is going on. So any help would be very much appreciated.
It's been a while since this erupted but thought to leave a note here for whoever will face the same issue...
On compact width devices, upon segueing from master to detail, the detail navigation controller is on top of the master view controller, unlike regular width where the two navigation controllers have their own separate root view controllers.
So, the UINavigationController of the detail view controller needs to be removed upon segue in combact width devices using UISplitViewControllerDelegate method: splitViewController(_:showDetail:sender:)
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if splitViewController.isCollapsed, let navController = vc as? UINavigationController {
if let detailVC = navController.topViewController {
splitViewController.showDetailViewController(detailVC, sender: sender)
return true
}
}
return false
}
Related
I have an app with a HomeView and a bottom TabBar which load other views on tap. On this TabBar bar I also have a More button which on click opens a listView, each cell of this view also opens other views. (When I'm talking about views, I mean new tunnels/navigation controllers are displayed for example: MyPicturesViewController, SettingsViewController etc...).
There is my problem, I have to implement a feature that from my HomeView on a button click it navigates to a specific view that is not on the TabBar but in the More list and also update the TabBar to highlight the More button.
I tried at first to navigate manually with this code:
let viewController = UIStoryboard.instantiate(NewViewController.self)
self.viewController?.navigationController?.pushViewController(viewController, animated: true)
it works pretty well
and then update the tabbar manually
self.viewController?.tabBarController?.selectedIndex = 4
But updating the TabBar redirect automatically to the selected index which is the More list view and don't even take in count the manual navigation I did in the previous code.
So my question is, is it possible to update TabBar bar without loading its own navigationcontroller? If not how can I achieve what I want to do, I tried many things such as create a navigationcontroller and its viewcontrollers and replace the tabbar viewcontrollers stacks but I didn't succeed.
Thank you in advance for those who will take time to help me!
Instead doing it before
self.viewController?.tabBarController?.selectedIndex = 4
Push your controller in tabBar delegate method
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewController == desiredNavigationController {
show(desiredNavigationController, animated: true) // push
return false
}
return true
}
I am working on a new project of mine and am looking for what the best solution to keep the UITabBarController displaying even when using a segue to push to a UIViewController.
Currently I have an Initiation of our UITabBarController on app launch, which contains multiple viewcontroller relationships. Particularly the initial view controller option is a custom UIViewController which implements a UITableView. Once a cell is selected I call a show(push) segue to another UIViewController. This is where I lose my TabBar which is as expected. Now I have tried different approaches such as setting the hidden value of our tabbar to YES, but does not seem to help.
Code
When Cell Selected:
[self performSegueWithIdentifier: #"tableCellOptions" sender: self];
When preparing for segue:
if([segue.identifier isEqualToString:#"tableCellOptions"]) {
additionUITableView *move = (additionUITableView *) segue.destinationViewController;
move.thisOption = [menuOptions objectAtIndex:cellPushed];
}
What would your approach be to this and why?
If you have Tab bar controller as the initial View Controller, the tab bar will show by default on each view controller.
If you are using storyboard or xib file, then select the tab bar item in view controller and check its properties, and make sure "hide tab bar on push" is unchecked.
Programmatically you can do this,
self.hidesBottomBarWhenPushed = NO;
[self.navigationController pushViewController:viewControllerToPush animated:YES];
Place it in viewDidLoad or viewDidAppear.
I hope this solves your problem.
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.
I have this application which uses internally a UISplitViewControler to display the main interface. The problem I have is that on IOS7 I don't see the button on the left to open the master panel.
The theory says that I have to set the delegate and the button will appear. In practice - my delegate is not called in IOS7. It does on IOS8.
First try:
I am following the normal double navigation controller scheme (described here: http://whoisryannystrom.com/2014/11/17/UISplitViewController-iOS-7/)
Code is swift :)
As I need my app to work on IOS7 phones, in am not creating the split view controller in code, but using the one in the storyboard:
(somewhere in app delegate):
UIStoryboard *board = [UIStoryboard storyboardWithName:#"Storyboard" bundle:nil];
UIViewController *newController = [board instantiateViewControllerWithIdentifier:#"LoginViewController2"];
self.window.rootViewController = newController;
The delegate is created in the master, and assigned to master. This works on IOS8.
Code in the master
override func akaweFromNib() {
super.awakeFromNib()
if let splitViewController = self.splitViewController {
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as UINavigationController
if (splitViewController.respondsToSelector(Selector("displayModeButtonItem"))) {
navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
}
splitViewController.delegate = self
}
}
This works, but I have to open the drawer and choose something on the master view (create a new segue) in order to see the button.
Second try
As this did not work - I created a new UISplitViewController and set the split view controller on the storyboard to this new class. Move the onWakeFromNib to this new class (and set the delegate as before). New code works on IOS8, but under IOS7 (at least on the IPad Emulator) the new class is not used for the split view controller - I don't hit a breakpoint in the new code.
What am I doing wrong?
Edit:
While copying code here, I forgot to mention that I am doing:
navigationItem.leftItemsSupplementBackButton = true
navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem()
But - this is only available in IOS8. What can I do in IOS7?
2015-02-12 10:37:55.987 OlympiaTracking[92551:607] -[UISplitViewController displayModeButtonItem]: unrecognized selector sent to instance 0x7b67f1c0
Edit 2:
I also followed ios7 no displayModeButtonItem or targetDisplayModeForActionInSplitViewController which works, but only after the first segue. When the controller is first displayed, the button is not visible.
Open this link and move to the iPad part. Where it says
Notice that when the iPad app is first opened up, there is no indication that this is a split view controller at all! To trigger the Master view controller, the user has to magically know to swipe left to right.
Even when the navigation controller is in place, the UI is not that
much better at first glance (although seeing a title is definitely an
improvement):
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];
}