The home page of my app has UIButtons, btnIncome and btnExpense. Pressing on this buttons pushes IncomeVC and ExpenseVC respectevely,which are two UIViewControllers with UITabBar added via xib. The tabBar have 4 items. Selecting on each tab item adds same four view controllers(which contains UITableViews) as the subview of IncomeVC and ExpenseVC,like for eg, DailyVC,WeeklyVC,MonthlyVC,YearlyVC.(ie,for Income ,there is daily,weekly etc and same for Expense) (I have done like that because the IncomeVC and ExpenseVC have a UIButton and a UIView which is common for all tabs).
So the problem is that, if click the btnIncome I have to populate those tableviews with the arrays related to Income and vice versa for Expense. How can I find from which viewController I selected the different tabs(I need to get it from the 4 Views I added as the subview of IncomeVC and ExpenseVC). Do I have to make 8 different views 4 each for Income and expense ?
Thanx.
In Swift 3,
if let navController = self.navigationController, navController.viewControllers.count >= 2 {
let viewController = navController.viewControllers[navController.viewControllers.count - 2]
}
You can get the previous viewController like following code,
NSLog(#"%#",[self.navigationController.viewControllers objectAtIndex:self.navigationController.viewControllers.count-2]);
This will displays the previous viewController name...
In Swift:
let n: Int! = self.navigationController?.viewControllers?.count
let myUIViewController = self.navigationController?.viewControllers[n-2] as! UIViewController
Swift 3
Here is a mashup of the previous answers that can be put into an extension:
extension UIViewController{
var previousViewController:UIViewController?{
if let controllersOnNavStack = self.navigationController?.viewControllers, controllersOnNavStack.count >= 2 {
let n = controllersOnNavStack.count
return controllersOnNavStack[n - 2]
}
return nil
}
}
Edit:
When fetching the previousViewController of a given view controller, call it VC1, in viewWillDisappear, VC1 is already popped of the Navigation Controller Stack. So in this scenario, the above code does not end up fetching the View controller directly above VC1(call it VC2), but the view controller above VC2 (if it exists).
To avoid this problem I just check if VC1 is still on the stack when previousViewController is requested. Here is the updated code:
extension UIViewController{
var previousViewController:UIViewController?{
if let controllersOnNavStack = self.navigationController?.viewControllers{
let n = controllersOnNavStack.count
//if self is still on Navigation stack
if controllersOnNavStack.last === self, n > 1{
return controllersOnNavStack[n - 2]
}else if n > 0{
return controllersOnNavStack[n - 1]
}
}
return nil
}
}
This code assumes that view controller you are sending the previousViewController message to will either be at the top of the navigation stack or not at all.
Swift 5.1 🔸
(Based on previous answers but simpler)
extension UINavigationController {
var previousViewController: UIViewController? {
viewControllers.count > 1 ? viewControllers[viewControllers.count - 2] : nil
}
}
If the reason for needing access to the previous view controller is to know what data to get, I would suggest that you instead give the new view controller the data before you push it on the stack. Or at least enough data so that the view controller know how to get the right data, e.g. a enum or constant or something.
This could be done with a custom initializer, or a property.
Take a look at this blog post for an example: "Passing Data Between View Controllers"
If you are using a storyboard, you can use prepareForSegue:sender to pass the right data. A good tutorial on that can be found here: Beginning Storyboards in iOS 5 Part 2
UIViewController *previousViewController = [[[self navigationController]viewControllers] objectAtIndex:([viewControllers indexOfObject:self]-1)];
Where self will be current view controller.
[viewControllers.count - 2] approach is buggy
If stated view controller is not last view controller of the navigation stack,[viewControllers.count - 2] returns incorrect result.
New crash safe solution:
Swift
extension UIViewController {
var previousViewController: UIViewController? {
guard
let viewControllers = navigationController?.viewControllers,
let index = viewControllers.firstIndex(of: self),
index > 0
else { return nil }
return viewControllers[index - 1]
}
}
Swift
extension UINavigationController {
func getPreviousViewController() -> UIViewController? {
let count = viewControllers.count
guard count > 1 else { return nil }
return viewControllers[count - 2]
}
}
You can certainly reuse the view controllers. I would suggest instead of pushing UIViewController (which contains UITabBar added from IB) when the button is pressed, you can push UITabBarController which contains 4 View Controllers (daily, weekly, monthly, and yearly).
You UITabBarController could have an enum property (Income and Expense) and this can be assigned when you push this UITabBarController based on the button pressed. From there, you can assign another enum property (Income and Expense) to the 4 UIViewControllers to show the correct type of data in each view controller.
Check out this post: How to identify previous view controller in navigation stack
Tony is right, you can get the previous VC from the viewControllers array but at index n-2.
Related
I'm working on an app that displays a today extension with some information. When I tap on the today extension, it opens the app and navigates to a subview from the root to display the information. Normally the user would then click the back arrow to go back to the main view, but there is no way to tell if this is actually done. It is possible for the user to go back to the today extension and tap again. When this is done, the subview is opened once again with new information. If this is done a bunch of times, I end up with a bunch of instances of the subview and I have to click the back button on each of them to get back to the main view.
My question: Is it possible to check if the subview is already visible? I'd like to be able to just send updated information to it, instead of having to display an entirely new view.
I am currently handling this by keeping the instance of the UIViewController at the top of my root. If it is not nil, then I just pass the information to it and redraw. If it is nil, then I call performSegue and create a new one.
I just think that there must be a better way of handling this.
Edit: Thanks to the commenter below, I came up with this code that seems to do what I need.
if let quoteView = self.navigationController?.topViewController as? ShowQuoteVC {
quoteView.updateQuoteInformation(usingQuote: QuoteService.instance.getQuote(byQuoteNumber: quote))
}
else {
performSegue(withIdentifier: "showQuote", sender: quote)
}
This is different from the suggested post where the answer is:
if (self.navigationController.topViewController == self) {
//the view is currently displayed
}
In this case, it didn't work because I when I come in to the app from the Today Extension, it goes to the root view controller. I needed to check whether a subview is being displayed, and self.navigationController.topViewcontroller == self will never work because I am not checking to see if the top view controller is the root view controller. The suggestions in this post are more applicable to what I am trying to accomplish.
u can use this extension to check for currently displayed through the UIApplication UIViewController:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
and usage example:
if let topController = UIApplication.topViewController() {
if !topController.isKind(of: MainViewController.self) { //MainViewController- the controller u wish to equal its type
// do action...
}
}
I'm trying to implement an IOS swift app which starts off with a tab bar controller, and in one of the tab bar controllers is an item called "account".
When a user presses the account item, I want the app to decide (onclick event) whether the view that contains sign up/login is displayed or the profile view is displayed based on a global variable "loggedIn" (bool type).
(I've tried navigation controller but what I've understood from that is that it is a sequence of views which can't decide between views)
I want to know how can this be implemented, maybe some kind of "router" if you may that can switch between views...
If you didn't understand here's a picture of what I'm trying to implement
Basic Map of what I'm trying to explain
If you can suggest a more professional way of doing such design please don't hesitate to express your opinion.
I believe a good approach is updating the view controller when loggedIn status changes. If you don't already have, create a class inheriting from UITabBarController to manage your tabs. Here's the code:
class TabController: UITabBarController {}
In your storyboard, select your tab controller, go to the Identity Inspector and set TabController as the custom class. Now TabController will manage all the view controllers in your tab bar.
It's usually not a good approach to use global variables, so let's add loggedIn in the scope of TabController and listen for any of changes in it and update the corresponding view controller:
class TabController: UITabBarController {
var loggedIn = true {
didSet {
updateProfileTab()
}
}
}
Now, whenever you change loggedIn, that change will update the proper tab. Now let's write updateProfileTab():
class TabController: UITabBarController {
func updateProfileTab() {
let viewController: UIViewController
if loggedIn {
viewController = makeProfileViewController()
} else {
viewController = makeLoginViewController()
}
setViewController(viewController, at: 2)
}
func makeProfileViewController() -> ProfileViewController {
// create and return the profile view controller
}
func makeLoginViewController() -> LoginViewController {
// create and return the profile view controller
}
}
Naturally, you might want to write the body of both makeProfileViewController and makeLoginViewController methods. The last thing for TabController is to write setViewController(_:at:) method:
class TabController: UITabBarController {
...
func setViewController(_ viewController: UIViewController, at index: Int) {
let tabBarItem = viewControllers?[index].tabBarItem
viewController.tabBarItem = tabBarItem
viewControllers?[index] = viewController
}
...
}
Now, since TabController manages your tab bar, you can access it from within any of its children view controllers:
guard let tabController = tabBarController as? TabController else { return }
tabController.loggedIn = ...
Also, it's important to select the initial state. So, in the viewDidLoad from one of your tabbed view controllers, you should perform the above code. The first tab (the one that displays first) is probably the best place to do that. Hope this helps!
EDIT
To create your login and signup view controllers, the easiest way to go is by assigning ids in your storyboard. To do that, go to your storyboard, select the view controller, and in the Identity Inspector set a Storyboard ID, which you will use to instantiate the view controller:
func makeProfileViewController() -> ProfileViewController {
let controller = self.storyboard!.instantiateViewController(withIdentifier: "theStoryboardID")
return controller as! ProfileViewController
}
Note that I'm using force unwrap here (!). That's just for brevity. In a real case scenario you will want to use some if let or guard let statements to handle nil values.
I've got a ViewControllerOne. ViewControllerOne is connected via Ctrl-Drag (in storyboard) to a menu-button mBtn (which means I don't know how it is implemented programmatically).
Clicking on this mBtn, a ViewOne appears (present modal). This ViewOne is bound to ViewControllerOne. ViewOne has a button btnOne.
Clicking on btnOne I want ViewOne to be dismissed and ViewTwo to be shown. ViewTwo belongs to ViewControllerTwo and to WindowControllerTwo.
The WindowControllerTwo-ViewControllerTwo-binding is the standard case as created on a new project.
I have the following code in the IBAction for button btnOne in ViewControllerOne:
#IBAction func onbtnOnePressed(sender: AnyObject){
let m_WindowControllerTwo = NSStoryboard(name: NSStoryboard.Name(rawValue: "Main"), bundle: nil).instantiateController(withIdentifier: NSStoryboard.SceneIdentifier("WindowControllerTwo")) as! NSWindowController // I have no custom class for the window controller as I don't really know what I can use it for ...
let m_ViewTwo = WindowControllerTwo.contentViewController as! ViewControllerTwo // my custom class for ViewTwo
m_ViewTwo.attributeIWantToPassToThisView = self.x // an attribute I want to pass from view a to view b
m_WindowControllerTwo.contentViewController = m_ViewTwo // passing the attribute from a to b
m_WindowControllerTwo.showWindow(self) // this does not work
self.dismiss(nil) // see NOTE
}
This code actually does not work. On debugging it step by step, I'm seeing the window/view flickering but not appearing...
NOTE: I could connect the button btnOne with a ctrl-drag to ViewControllerTwo. This works. But then the current ViewOne does not get dismissed!
Question: What am I doing wrong here? In iOS swift this also works. I don't quite get the WindowController stuff, so I'll need your advice on this.
Instead of this: m_WindowControllerTwo.showWindow(self)
use:
let application = NSApplication.shared()
application.runModal(for: wordCountWindow) //this will present WindowControllerTwo modally.
then to close your present controller add this line: PresentWindowControllerName.close()
I have an app written in swift that displays location information on different views. I am receiving location updates in AppDelegate (I don't want to use a singleton class since I want the location updates even when the app is in the background).
Now, I am using SWRevealViewController to implement a sidebar menu to toggle between the different views. When a new location update is received, how do I call the function of the viewController that is currently active to update the UI?
I searched a lot and all the solutions that talk about how to find the current UIViewController actually return SWRevealViewController as the current UIViewController, which doesn't help.
I gave it another shot and was able to find my viewController in my app from another view controller. However, I couldn't get my child view controllers app delegate.
Looking forward to other answers...
here's what I did:
let app = UIApplication.sharedApplication().delegate! as! AppDelegate
if let nav = app.window?.rootViewController?.childViewControllers {
nav.forEach { vc in
if let viewControllers = vc as? UINavigationController {
viewControllers.childViewControllers.forEach { vc in
if let x = vc as? ViewController {
print("Got it \(x)")
}
}
}
}
}
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
}