Change order in Navigation Controller - ios

I'm working with Swift3. I have an App with the VCs as in the picture.
In the Mainmenu-VC the user triggers the Input-segue. User enters a firstname in the Input-VC. This triggers the Select-segue to Select-VC to select a surname and trigger Selected-segue to Details-VC.
From the Mainmenu-VC the user can also access the Details-VC. Back via NavigationControllerMechanism to Mainmenu-VC.
I want to change the NavigationControllerMechanism 'history', so that when the user enters from the Details-VC via the Selected-segue, the previous VC is changed from Select-VC to Mainmenu-VC.
So basically when in the Details-VC, the Back always returns to Mainmenu-VC.
I have tried combining various solutions from the web, without succes.
Is this possible?

Yes it is.
The View-Controller stack is stored in currentViewController.navigationController?.viewControllers.
So you should make something like :
//In Your Details VC :
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let stack = self.navigationController?.viewControllers else { return }
//get the mainMenu VC
let mainVC = stack.first!
// Rearrange your stack
self.navigationController?.viewControllers = [mainVC, self]
//Now you can press "bac" to Main VC
}

you want to change navigation stack in this way you can manipulate
let myprofile = storyboard.instantiateViewController(withIdentifier: "Profile1ViewController") as! Profile1ViewController
let sourseStack = self.navigationController!.viewControllers[0];
var controllerStack = self.navigationController?.viewControllers
let index = controllerStack!.index(of: sourseStack);
controllerStack![index!] = myprofile
self.navigationController!.setViewControllers(controllerStack!, animated: false);
to go to RootViewController
dispatch_async(dispatch_get_main_queue(), {
self.navigationController?.popToRootViewControllerAnimated(true)
})

Related

ios - Change tabs and open modal window dynamically

I have an app that sends local notiffications at specific times. When the user taps on the notification, the app should open on a particular tab (a library tab) and then open automatically one of the lessons (without the user having to tap on it).
I can get the app to switch to the Library tab, but the modal with the lesson never appears.
This is the code I'm using:
if let tabBarController = window.rootViewController as? UITabBarController {
// this works
tabBarController.selectedViewController!.dismiss(animated: true, completion: nil)
tabBarController.selectedViewController = tabBarController.viewControllers?[1]
// Segue to particular lesson
let vc = tabBarController.selectedViewController as? LibraryViewController
let lessonVC = LessonViewController()
let les60 = Lesson(lessonNumber: "60")
lessonVC.delegate = vc
lessonVC.lesson = les60
vc?.present(UINavigationController(rootViewController: lessonVC), animated: true)
}
This is the code I use for every item in the Library:
let lessonVC = LessonViewController()
lessonVC.delegate = self
lessonVC.lesson = lessonsArray[indexPath.row]
present(UINavigationController(rootViewController: lessonVC), animated: true)
so I tried to replicate it above but it does not work.

passing data from 2 view controllers without segue

I have a mainviewcontroller and a popup view controller which opens without a segue.
the popup viewcontroller recive data from Firebase, and then i need to append this data to an array in the mainviewcontroller.
How can i do that?
(i tried to create a property of the popupviewcontroller in the mainviewcontroller, but in crashes the app)
this is the code that opens the popup:
#IBAction func showPopUp(_ sender: Any) {
let popOverVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "sbPopUp") as! PopUpViewController
self.addChild(popOverVC)
popOverVC.view.frame = self.view.frame
self.view.addSubview(popOverVC.view)
popOverVC.didMove(toParent: self)
You need to connect the classes so that the popup class knows what to do with the data once it has been received. Here's a sample structure that works in a playground that you should be able to apply to your real classes.
class MainClass {
func showPopUp() {
let popOverVC = PopUpClass()
popOverVC.update = addToArray
popOverVC.receivedData()
}
func addToArray() {
print("Adding")
}
}
class PopUpClass {
var update: (()->())?
func receivedData() {
if let updateFunction = update {
updateFunction()
}
}
}
let main = MainClass()
main.showPopUp()
Or you could create a global variable so it can be accessed anywhere. ... It is better to pass data but I just thought it would be easier in this instance, so the variable can be accessed anywhere in your entire project.
if it is just a notification, you can show it in an alert, but if you don't want to use an alert my offer to present another view controller is not like this , try my code :
//if you have navigation controller ->
let vc = self.storyboard?.instantiateViewController(
withIdentifier: "storyboadrID") as! yourViewController
self.navigationController?.pushViewController(vc, animated: true)
//if you don't use navigation controller ->
let VC1 = self.storyboard!.instantiateViewController(withIdentifier: "storyboadrID") as! yourViewController
self.present(VC1, animated:true, completion: nil)
also if you want to pass any data or parameter to destination view controller you can put it like this before presenting your view controller :
VC1.textView.text = "test"

How to pass data to nth view controller on the UINavigationController stack?

There are 2 apps A & B.
App A has a url to open App B.
Soon as App B opens, it has to load 5 view controllers onto the navigation stack which is done by the following code:
let LandingVC = self.storyboard?.instantiateViewControllerWithIdentifier("LandingVC") as! LandingVC
let Dashboard = self.storyboard?.instantiateViewControllerWithIdentifier("Dashboard") as! Dashboard
let PlayerVC = self.storyboard?.instantiateViewControllerWithIdentifier("PlayerVC") as! PlayerVC
let PlayerDetailVC = self.storyboard?.instantiateViewControllerWithIdentifier("PlayerDetailVC") as! PlayerDetailVC
let ScoreReportVC = self.storyboard?.instantiateViewControllerWithIdentifier("ScoreReportVC") as! ScoreReportVC
let viewControllersList = [LandingVC, Dashboard, PlayerVC, PlayerDetailVC, ScoreReportVC]
self.navigationController?.setViewControllers(viewControllersList, animated: false)
From ScoreReportVC I need to be able to set variables on previous ViewControllers so that the user can navigate to previous screens even though they fired the application from another application.
Here is what I have tried: defined a protocol in previous view controllers that are behind ScoreReportVC that are on the stack and inside ScoreReportVC like so:
for viewcontroller in self.navigationController?.viewControllers {
if viewcontroller is PlayerDetailVC {
PlayerDetailVC.delegate = self
}
if viewcontroller is PlayerVC {
PlayerVC = self
}
if viewcontroller is Dashboard {
Dashboard.delegate = self
}
if viewcontroller is LandingVC {
LandingVC.delegate = self
}
}
But the delegates are not getting called. Any help how to correctly pass data to all the ViewControllers on the stack would be greatly appreciated.
For relatable classes we use Delegations. But the classes which are not relatable , for those, we use Notifications. In your case Notifications will be needed for implementation and to pass data from one VC to another VC.
A better approach would be to create/set the variables when the controllers are created.

Opening ViewController In AppDelegate While Keeping Tabbar

In my Xcode project when a user taps on a notification I want to first send them to a certain item in my tabBar then I want to instantiate a view controller and send an object over to that view controller. I have code the that sends them to the tabBar I want, but I do not know how to instantiate them to the view controller while keeping the tabBar and navigation bar connected to the view controller. All the answers on this require you to change the root view controller and that makes me lose connection to my tabBar and navigation bar when the view controller is called.
A Real Life Example of this: User receives Instagram notification saying "John started following you" -> user taps on notification -> Instagram opens and shows notifications tab -> quickly send user to "John" profile and when the user presses the back button, it sends them back to the notification tab
Should know: The reason why I'm going to a certain tab first is to get that tab's navigation controller because the view controller I'm going to does not have one.
Here's my working code on sending the user to "notifications" tab (I added comments to act like the Instagram example for better understanding):
if let tabbarController = self.window!.rootViewController as? UITabBarController {
tabbarController.selectedViewController = tabbarController.viewControllers?[3] //goes to notifications tab
if type == "follow" { //someone started following current user
//send to user's profile and send the user's id so the app can find all the information of the user
}
}
First of all, you'll to insatiate a TabBarController:
let storyboard = UIStoryboard.init(name: "YourStoryboardName", bundle: nil)
let tabBarController = storyboard.instantiateViewController(withIdentifier: "YourTabBarController") as! UITabBarController
And then insatiate all of the viewControllers of TabBarController. If your viewControllers is embedded in to the UINavigationController? If so, you'll to insatiate a Navigation Controller instead:
let first = storyboard.instantiateViewiController(withIdentifier: "YourFirstNavigationController") as! UINavigationController
let second = storyboard.instantiateViewiController(withIdentifier: "YourSecondNavigationController") as! UINavigationController
let third = storyboard.instantiateViewiController(withIdentifier: "YourThirdNavigationController") as! UINavigationController
Also you should instantiate your desired ViewController too:
let desiredVC = storyboard.instantiateViewController(withIdentifier: "desiredVC") as! ExampleDesiredViewController
Make all of the NavigationControllers as viewControllers of TabBarController:
tabBarController.viewControllers = [first, second, third]
And check: It's about your choice.
if tabBarController.selectedViewController == first {
// Option 1: If you want to present
first.present(desiredVC, animated: true, completion: nil)
// Option 2: If you want to push
first.pushViewController(desiredVC, animated. true)
}
Make tabBarController as a rootViewController:
self.window = UIWindow.init(frame: UIScreen.main.bounds)
self.window?.rootViewController = tabBarController
self.window?.makeKeyAndVisible()
Finally: It's your completed code:
func openViewController() {
let storyboard = UIStoryboard.init(name: "YourStoryboardName", bundle: nil)
let tabBarController = storyboard.instantiateViewController(withIdentifier: "YourTabBarController") as! UITabBarController
let first = storyboard.instantiateViewiController(withIdentifier: "YourFirstNavigationController") as! UINavigationController
let second = storyboard.instantiateViewiController(withIdentifier: "YourSecondNavigationController") as! UINavigationController
let third = storyboard.instantiateViewiController(withIdentifier: "YourThirdNavigationController") as! UINavigationController
let desiredVC = storyboard.instantiateViewController(withIdentifier: "desiredVC") as! ExampleDesiredViewController
tabBarController.viewControllers = [first, second, third]
if tabBarController.selectedViewController == first {
// Option 1: If you want to present
first.present(desiredVC, animated: true, completion: nil)
// Option 2: If you want to push
first.pushViewController(desiredVC, animated. true)
}
self.window = UIWindow.init(frame: UIScreen.main.bounds)
self.window?.rootViewController = tabBarController
self.window?.makeKeyAndVisible()
}
If you want to present or push ViewController when the notification is tapped? Try something like that:
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
switch response.actionIdentifier {
case UNNotificationDefaultActionIdentifier:
openViewController()
completionHandler()
default:
break;
}
}
}
I can think of two ways to do that:
1) If that view controller is a UINavigationController you can simply push the profile from wherever you are:
if let tabNavigationController = tabbarController.viewControllers?[3] as? UINavigationController {
tabbarController.selectedViewController = tabNavigationController
let profileViewController = ProfileViewController(...)
// ... set up the profile by setting the user id or whatever you need to do ...
tabNavigationController.push(profileViewController, animated: true) // animated or not, your choice ;)
}
2) Alternatively, what I like to do is control such things directly from my view controller subclass (in this case, PostListViewController). I have this helper method in a swift file that I include in all of my projects:
extension UIViewController {
var containedViewController: UIViewController {
if let navController = self as? UINavigationController, let first = navController.viewControllers.first {
return first
}
return self
}
}
Then I would do this to push the new view controller:
if let tabViewController = tabbarController.selectedViewController {
tabbarController.selectedViewController = tabViewController
if let postListViewController = tabViewController.containedViewController as? PostListViewController {
postListViewController.goToProfile(for: user) // you need to get the user reference from somewhere first
}
}
In my last live project, I'm using the same approach like yours. So even though I doubt this method is the correct or ideal for handling a push notification from the AppDelegate (I still got a lot of stuff to learn in iOS 🙂), I'm still sharing it because it worked for me and well I believe the code is still readable and quite clean.
The key is to know the levels or stacks of your screens. The what are childViewControllers, the topMost screen, the one the is in the bottom, etc...
Then if you're now ready to push to a certain screen, you would need of course the navigationController of the current screen you're in.
For instance, this code block is from my project's AppDelegate:
func handleDeeplinkedJobId(_ jobIdInt: Int) {
// Check if user is in Auth or in Jobs
if let currentRootViewController = UIApplication.shared.keyWindow!.rootViewController,
let presentedViewController = currentRootViewController.presentedViewController {
if presentedViewController is BaseTabBarController {
if let baseTabBarController = presentedViewController as? BaseTabBarController,
let tabIndex = TabIndex(rawValue: baseTabBarController.selectedIndex) {
switch tabIndex {
case .jobsTab:
....
....
if let jobsTabNavCon = baseTabBarController.viewControllers?.first,
let firstScreen = jobsTabNavCon.childViewControllers.first,
let topMostScreen = jobsTabNavCon.childViewControllers.last {
...
...
So as you can see, I know the hierarchy of the screens, and by using this knowledge as well as some patience in checking if I'm in the right screen by using breakpoints and printobject (po), I get the correct reference. Lastly, in the code above, I have the topMostScreen reference, and I can use that screen's navigationController to push to a new screen if I want to.
Hope this helps!

Why are my UIBarButtons not showing in the navigation bar? (Possible iOS 11 bug?)

If I perform segues to my VC, the UIBarButtons will show. However, if I programmatically push to the view controller, then the UIBarButtons will not show. Bug is not present in iOS 9 or iOS 10.
This code exists in the first view controller that is presented when the app opens. I am checking UserDefaults to see if I should restore what we are calling an "interaction", and I am programmatically creating the stack. Because of issues I was having with restoring the application state, I have reverted to this method.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if UserDefaults.standard.bool(forKey: "ShouldRestoreInteraction") {
if
let uri = UserDefaults.standard.url(forKey: "InteractionObjectURI"),
let id = CoreDataUtils.coord.managedObjectID(forURIRepresentation: uri),
let interactionObject = CoreDataUtils.context.object(with: id) as? InteractionObject
{
let sb = UIStoryboard(name: "Interaction", bundle: nil)
let interactionCreateOrEdit = sb.instantiateViewController(withIdentifier: "InteractionUnified") as! InteractionCreateOrEdit
interactionCreateOrEdit.interaction = Interaction(object: interactionObject)
interactionCreateOrEdit.interactionObject = interactionObject
if UserDefaults.standard.bool(forKey: "RestoreToDashboard") {
// restore to dashboard
let sb = UIStoryboard(name: "MyDashboard", bundle: nil)
let dashNav = sb.instantiateInitialViewController() as! UINavigationController
let dashboardTVC = sb.instantiateViewController(withIdentifier: "DashboardTableViewController")
self.revealViewController().setFront(dashboardTVC, animated: false)
dashNav.pushViewController(dashboardTVC, animated: false)
dashNav.pushViewController(interactionCreateOrEdit, animated: false)
navigationController!.present(dashNav, animated: false, completion: nil)
} else {
print("Error restoring interaction - did not specify bottom controller in stack")
}
} else {
print("Error restoring interaction - object in core data no longer exists")
}
}
}
However, the weird thing is if I click "Debug View Hierarchy", then the UIBarButtons will display on the phone, but not in the Debug View Hierarchy. If I continue program execution, then hit Debug View Hierarchy again, the buttons will show in the DVH. Additionally, after continuing program execution, the UIBarButtons continue to show in the navigation bar.
Looks like Mr. Matt was right in his "assurance". Not sure why my code works for previous versions of iOS...anyway solution is as follows:
if UserDefaults.standard.bool(forKey: "RestoreToDashboard") {
// restore to dashboard
let sb = UIStoryboard(name: "MyDashboard", bundle: nil)
let dashNav = sb.instantiateInitialViewController() as! UINavigationController
let dashboardTVC = sb.instantiateViewController(withIdentifier:"DashboardTableViewController")
dashNav.setViewControllers([dashboardTVC, interactionCreateOrEdit], animated: false)
self.revealViewController().setFront(dashNav, animated: false)
}

Resources