UISplitView's MasterViewController and Navigation Issue - ios

I am updating my existing app to include a SplitView for iPads.
I have it working with a UITabBar, but am having an issue with my masterViewController as it is generating a "duplicate" navigation bar that is covering my existing navigation items on all masterViewControllers (tabs), including searchBar on the search tab.
The code I have is:
AppDelegate
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
{
let splitViewController = self.window!.rootViewController as! UISplitViewController
splitViewController.delegate = self
splitViewController.preferredPrimaryColumnWidthFraction = 0.33
splitViewController.minimumPrimaryColumnWidth = 375
splitViewController.preferredDisplayMode = .allVisible
return true
}
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
return true
}
The reason for having this in the AppDelegate is I saw an example where placing it hear will allow me not to require the code in each of the different Master Views (each tab). Have yet to test this as still working on the first master view.
Master View
override func viewDidLoad()
{
self.extendedLayoutIncludesOpaqueBars = true
self.navigationItem.hidesBackButton = true
// 3D Touch
if traitCollection.forceTouchCapability == .available {
registerForPreviewing(with: self as UIViewControllerPreviewingDelegate, sourceView: view)
ThreeDTouch = true
}
self.addSwitchVewButtonToNavigationBar()
self.addCategoryButtonToNavigationBar()
}
func addSwitchVewButtonToNavigationBar() {
let switchButton = UIButton(type: UIButtonType.custom)
let editImage = UIImage(named: "CollectionButton")?.withRenderingMode(.alwaysTemplate)
switchButton.setImage(editImage, for: .normal)
switchButton.addTarget(self, action: #selector(SpeciesViewController.onSwitchView), for: UIControlEvents.touchUpInside)
let switchButtonFinal = UIBarButtonItem(customView:switchButton)
self.navigationItem.rightBarButtonItem = switchButtonFinal
}
#IBAction func onSwitchView(_ sender: UIBarButtonItem)
{
AppDelegate.getAppState().isListViewSelected = false
let speciesColletion = storyboard?.instantiateViewController(withIdentifier: Resource.SpeciesCollectionStoryboard) as! SpeciesCollectionViewController
self.navigationController?.viewControllers = [speciesColletion]
}
Originally, the onSwitchViewButton was embedded using the IB, but did not work. This is the same system used for the addFavorite on the Detail View.

The problem that you addressing wrong UINavigation controller. I assume before splitting both master and SpeciesViewController existed in same navigation environment, and use same navigation controller. But in split view they don't. Your detail controller is actually UINavigation controller you are looking for, that had to control all navigation, and had to have buttons you need. You can get it from master as:
guard let split = splitViewController, let navController = split.viewControllers.last as? UINavigationController else { return }
And make sure that you split controller not embedded into another UINavigationController (reason for second navigation bar).
EDIT:
Function to return detail's Nav Controller:
var detailsNavigationController: UINavigationController? {
return splitViewController?.viewControllers.last as? UINavigationController
}
To access blue call detailsNavController, to access red use navigationController.

Related

How to get MasterVC of a UISplitViewController to show when app is launched as

trying to get the MasterVC to show up when app is launched. Right now the DetailVC shows when app is launched. Here is my code for splitViewController. I'm certain the solution is in the Delegate method.
I'm using CoreData and the model is var items = [Notes]()
The ideal outcome is that when the app is launched, the MasterVC tableView is showing and by tapping on a tableCell the navigationController pushes to the DetailVC to display the indexPath data. The app already does this. Only need to have MasterVC load upon launch. Thank you.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let splitViewController = self.window!.rootViewController as! UISplitViewController
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
splitViewController.delegate = self as? UISplitViewControllerDelegate
return true
}
UISplitViewController Delegate
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return true }
let topAsDetailController = secondaryAsNavController.topViewController as? MasterViewController
if topAsDetailController?.items == nil {
return true
}
return false
}
You need to implement the splitViewController(_:collapseSecondary:onto:) method of the UISplitViewControllerDelegate protocol and return true:
Return Value
false to let the split view controller try and incorporate the
secondary view controller’s content into the collapsed interface or
true to indicate that you do not want the split view controller to do
anything with the secondary view controller.
The sample code that comes in a new Master-Detail App project:
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false }
if topAsDetailController.detailItem == nil {
// Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
return true
}
return false
}

Open Second TabBar view controller From Button Click of first sub view controller? [duplicate]

I need write some code to switch the view to another tab when the iOS app starts (so, for example, the second tab is shown by default rather than the first).
I'm new to Swift, and have worked out the following:
The code should probably go in the override func viewDidLoad() function of the ViewController of the first tab.
The following code shows the second ViewController, but not with the tab bar at the bottom (vcOptions is the second ViewController tab item:
let vc : AnyObject! = self.storyboard.instantiateViewControllerWithIdentifier("vcOptions")
self.showViewController(vc as UIViewController, sender: vc)
I think the answer may lie in using the UITabbarController.selectedIndex = 1, but not quite sure how to implement this.
If your window rootViewController is UITabbarController(which is in most cases) then you can access tabbar in didFinishLaunchingWithOptions in the AppDelegate file.
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
// Override point for customization after application launch.
if let tabBarController = self.window!.rootViewController as? UITabBarController {
tabBarController.selectedIndex = 1
}
return true
}
This will open the tab with the index given (1) in selectedIndex.
If you do this in viewDidLoad of your firstViewController, you need to manage by flag or another way to keep track of the selected tab. The best place to do this in didFinishLaunchingWithOptions of your AppDelegate file or rootViewController custom class viewDidLoad.
Swift 3
You can add this code to the default view controller (index 0) in your tabBarController:
override func viewWillAppear(_ animated: Bool) {
_ = self.tabBarController?.selectedIndex = 1
}
Upon load, this would automatically move the tab to the second item in the list, but also allow the user to manually go back to that view at any time.
1.Create a new class which supers UITabBarController.
E.g:
class xxx: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
}
2.Add the following code to the function viewDidLoad():
self.selectedIndex = 1; //set the tab index you want to show here, start from 0
3.Go to storyboard, and set the Custom Class of your Tab Bar Controller to this new class.
(MyVotes1 as the example in the pic)
The viewController has to be a child of UITabBarControllerDelegate. So you just need to add the following code on SWIFT 3
self.tabBarController?.selectedIndex = 1
To expand on #codester's answer, you don't need to check and then assign, you can do it in one step:
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
// Override point for customization after application launch.
if let tabBarController = self.window!.rootViewController as? UITabBarController {
tabBarController.selectedIndex = 1
}
return true
}
Swift 5
//MARK:- if you are in UITabBarController
self.selectedIndex = 1
or
tabBarController?.selectedIndex = 1
if you are using UITabBarController
// UITabBarDelegate
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
print("Selected item")
if kAppDelegate.isLoggedIn == false {
switch tabBar.selectedItem?.title {
case "Favorite":
DispatchQueue.main.async {
self.selectedIndex = 0
DispatchQueue.main.async {
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
self.showAlert(Constants.GlobalConstants.APP_NAME, message: "You are not login.")
}
}
}
break
case "Track Order":
DispatchQueue.main.async {
self.selectedIndex = 0
DispatchQueue.main.async {
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
self.showAlert(Constants.GlobalConstants.APP_NAME, message: "You are not login.")
}
}
}
break
default:
break
}
}
}
In a typical application there is a UITabBarController and it embeds 3 or more UIViewController as its tabs. In such a case if you subclassed a UITabBarController as YourTabBarController then you can set the selected index simply by:
selectedIndex = 1 // Displays 2nd tab. The index starts from 0.
In case you are navigating to YourTabBarController from any other view, then in that view controller's prepare(for segue:) method you can do:
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.
if segue.identifier == "SegueToYourTabBarController" {
if let destVC = segue.destination as? YourTabBarController {
destVC.selectedIndex = 0
}
}
I am using this way of setting tab with Xcode 10 and Swift 4.2.
Try
DispatchQueue.main.async {
self.tabBarController?.selectedIndex = 1
}
Just to update, following iOS 13, we now have SceneDelegates. So one might choose to put the desired tab selection in SceneDelegate.swift as follows:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
if let tabBarController = self.window!.rootViewController as? UITabBarController {
tabBarController.selectedIndex = 1
}
}
If you want to do this from code that you are writing as part of a particular view controller, like in response to a button press or something, you can do this:
#IBAction func pushSearchButton(_ sender: UIButton?) {
if let tabBarController = self.navigationController?.tabBarController {
tabBarController.selectedIndex = 1
}
}
And you can also add code to handle tab switching using the UITabBarControllerDelegate methods. Using tags on the base view controllers of each tab, you can see where you are and act accordingly: For example
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// if we didn't change tabs, don't do anything
if tabBarController.selectedViewController?.tabBarItem.tag == viewController.tabBarItem.tag {
return false
}
if viewController.tabBarItem.tag == 4096 { // some particular tab
// do stuff appropriate for a transition to this particular tab
}
else if viewController.tabBarItem.tag == 2048 { // some other tab
// do stuff appropriate for a transition to this other tab
}
}
Swift 5
If your class is a subclass of UITabBarController, you can just do:
selectedViewController = viewControllers![yourIndex]
From any where in-app you can switch between tabs
struct Constants{
static var tabbarVC : TabbarViewController!
}
During initialization of tab bar view controller store its reference to Constants.tabbarVC
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "TabbarViewController") as! TabbarViewController
Constants.tabbarVC = vc
Finally by using this reference you can switch tabs
Constants.tabbarVC.selectedIndex = 2

Swift: Setting Different Navigation Bar Buttons for Different Tabs

I created a custom UITabBarController with 5 tabs in Swift 3.0. I've been trying to set different navigation bar items for certain tabs, but it's not working well.
Situation: I put codes that would change the Navigation Bar Buttons in each tab's viewDidLoad().
//The customized Tab Bar controller instance. Contained in each of the tabs.
var mainController: TabBarController?
override func viewDidLoad() {
// ... more code
setupNavBar()
// ... more code
}
func setupNavBar() {
// ... more code
mainController?.navigationItem.leftBarButtonItem = UIBarButtonItem(image: friendsImage, style: .plain, target: self, action: #selector(handleFindFriends))
// ... more code
}
Problem: Let's say Tab #1 is supposed to have NavBarButton A and Tab #2 is supposed to have NavBarButton B.
When I switch from Tab #1 to Tab #2, the code works fine; the NavBarButton changes from A to B.
However, when I click on Tab #1, the NavBarButton still remains B.
How can I make it so that the navigation bar buttons change accordingly even when I click on a tab I was on previously?
I assume you embed a UITabBarController (or it's subclass) into a UIViewController. This is wrong because in this case you tend to make the view controller generic which is usually a bad practice.
Instead I would suggest to change hierarchy of view controller to what you see on the image below.
UPDATE
If you do it in code:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let tabBarController = UITabBarController()
// first tab
let firstViewController = UIViewController()
_ = firstViewController.view
firstViewController.title = "First"
let firstNavigationController = UINavigationController(rootViewController: firstViewController)
// sets a specific button ("Friends") on a navigation bar for the first screen
firstViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Friends", style: .plain, target: nil, action: nil)
// second tab
let secondViewController = UIViewController()
_ = secondViewController.view
secondViewController.title = "Second"
let secondNavigationController = UINavigationController(rootViewController: secondViewController)
// sets a specific button ("Photos") on a navigation bar for the second screen
secondViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Photos", style: .plain, target: nil, action: nil)
// embeds the navigation controllers into the tab bar controller
tabBarController.viewControllers = [firstNavigationController, secondNavigationController]
// creates a window with the tab bar controller inside
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = tabBarController
window?.makeKeyAndVisible()
return true
}
}
I actually found a solution XD
In TabBarController:
override func viewWillAppear(_ animated: Bool) {
//Original code to set up tabs
//Code I added:
for i in 0...4 {
tabBar.items?[i].tag = i
}
}
//Code I added:
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
//Code to run when a certain tab is selected
}
Still, thanks a lot!

UITabbarController UITabbarItem does not appear when UINavigationController is embeded

As the title says, does the UITabbarItem not appear if I embed in a UINavigationController like so:
lazy var tabBarViewController: UITabBarController = {
let tBarViewController = UITabBarController()
let firstViewController = ViewController()
firstViewController.tabBarItem.title = "Home"
tBarViewController.viewControllers = [UINavigationController(rootViewController: firstViewController)]
return tBarViewController
}()
lazy var window: UIWindow = {
let win = UIWindow(frame: UIScreen.mainScreen().bounds)
win.backgroundColor = UIColor.whiteColor()
win.rootViewController = self.tabBarViewController
return win
}()
func customizeAppereance() {
UINavigationBar.appearance().barTintColor = UIColor.themeColor()
UITabBar.appearance().barTintColor = UIColor.themeColor()
}
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
customizeAppereance()
UIApplication.sharedApplication().setStatusBarHidden(false, withAnimation: .Fade)
window.makeKeyAndVisible()
return true
}
If I remove the UINavigationController it works and the UITabbarItem is visible.
How can I embed in a UINavigationController and have the UITabbarItem to appear? (I am not using NIBs or Storyboard, just code) ?
The tab bar item has to belong to the view controller you're adding to the tab bar controller - in this case, the navigation controller. Otherwise, it tries to create one from the title (non-existent).
You need to set the tabBarItem property on the navigation controller, not its root view controller.

Programmatically switching between tabs within Swift

I need write some code to switch the view to another tab when the iOS app starts (so, for example, the second tab is shown by default rather than the first).
I'm new to Swift, and have worked out the following:
The code should probably go in the override func viewDidLoad() function of the ViewController of the first tab.
The following code shows the second ViewController, but not with the tab bar at the bottom (vcOptions is the second ViewController tab item:
let vc : AnyObject! = self.storyboard.instantiateViewControllerWithIdentifier("vcOptions")
self.showViewController(vc as UIViewController, sender: vc)
I think the answer may lie in using the UITabbarController.selectedIndex = 1, but not quite sure how to implement this.
If your window rootViewController is UITabbarController(which is in most cases) then you can access tabbar in didFinishLaunchingWithOptions in the AppDelegate file.
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
// Override point for customization after application launch.
if let tabBarController = self.window!.rootViewController as? UITabBarController {
tabBarController.selectedIndex = 1
}
return true
}
This will open the tab with the index given (1) in selectedIndex.
If you do this in viewDidLoad of your firstViewController, you need to manage by flag or another way to keep track of the selected tab. The best place to do this in didFinishLaunchingWithOptions of your AppDelegate file or rootViewController custom class viewDidLoad.
Swift 3
You can add this code to the default view controller (index 0) in your tabBarController:
override func viewWillAppear(_ animated: Bool) {
_ = self.tabBarController?.selectedIndex = 1
}
Upon load, this would automatically move the tab to the second item in the list, but also allow the user to manually go back to that view at any time.
1.Create a new class which supers UITabBarController.
E.g:
class xxx: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
}
2.Add the following code to the function viewDidLoad():
self.selectedIndex = 1; //set the tab index you want to show here, start from 0
3.Go to storyboard, and set the Custom Class of your Tab Bar Controller to this new class.
(MyVotes1 as the example in the pic)
The viewController has to be a child of UITabBarControllerDelegate. So you just need to add the following code on SWIFT 3
self.tabBarController?.selectedIndex = 1
To expand on #codester's answer, you don't need to check and then assign, you can do it in one step:
func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {
// Override point for customization after application launch.
if let tabBarController = self.window!.rootViewController as? UITabBarController {
tabBarController.selectedIndex = 1
}
return true
}
Swift 5
//MARK:- if you are in UITabBarController
self.selectedIndex = 1
or
tabBarController?.selectedIndex = 1
if you are using UITabBarController
// UITabBarDelegate
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
print("Selected item")
if kAppDelegate.isLoggedIn == false {
switch tabBar.selectedItem?.title {
case "Favorite":
DispatchQueue.main.async {
self.selectedIndex = 0
DispatchQueue.main.async {
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
self.showAlert(Constants.GlobalConstants.APP_NAME, message: "You are not login.")
}
}
}
break
case "Track Order":
DispatchQueue.main.async {
self.selectedIndex = 0
DispatchQueue.main.async {
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { timer in
self.showAlert(Constants.GlobalConstants.APP_NAME, message: "You are not login.")
}
}
}
break
default:
break
}
}
}
In a typical application there is a UITabBarController and it embeds 3 or more UIViewController as its tabs. In such a case if you subclassed a UITabBarController as YourTabBarController then you can set the selected index simply by:
selectedIndex = 1 // Displays 2nd tab. The index starts from 0.
In case you are navigating to YourTabBarController from any other view, then in that view controller's prepare(for segue:) method you can do:
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.
if segue.identifier == "SegueToYourTabBarController" {
if let destVC = segue.destination as? YourTabBarController {
destVC.selectedIndex = 0
}
}
I am using this way of setting tab with Xcode 10 and Swift 4.2.
Try
DispatchQueue.main.async {
self.tabBarController?.selectedIndex = 1
}
Just to update, following iOS 13, we now have SceneDelegates. So one might choose to put the desired tab selection in SceneDelegate.swift as follows:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }
if let tabBarController = self.window!.rootViewController as? UITabBarController {
tabBarController.selectedIndex = 1
}
}
If you want to do this from code that you are writing as part of a particular view controller, like in response to a button press or something, you can do this:
#IBAction func pushSearchButton(_ sender: UIButton?) {
if let tabBarController = self.navigationController?.tabBarController {
tabBarController.selectedIndex = 1
}
}
And you can also add code to handle tab switching using the UITabBarControllerDelegate methods. Using tags on the base view controllers of each tab, you can see where you are and act accordingly: For example
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// if we didn't change tabs, don't do anything
if tabBarController.selectedViewController?.tabBarItem.tag == viewController.tabBarItem.tag {
return false
}
if viewController.tabBarItem.tag == 4096 { // some particular tab
// do stuff appropriate for a transition to this particular tab
}
else if viewController.tabBarItem.tag == 2048 { // some other tab
// do stuff appropriate for a transition to this other tab
}
}
Swift 5
If your class is a subclass of UITabBarController, you can just do:
selectedViewController = viewControllers![yourIndex]
From any where in-app you can switch between tabs
struct Constants{
static var tabbarVC : TabbarViewController!
}
During initialization of tab bar view controller store its reference to Constants.tabbarVC
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "TabbarViewController") as! TabbarViewController
Constants.tabbarVC = vc
Finally by using this reference you can switch tabs
Constants.tabbarVC.selectedIndex = 2

Resources