Instantiating deep View Controller with Tab bar and Navigation - swift

I am using Xcode 11 and Swift 5.
Upon receiving an APNS notification, I need to jump to a view controller deep in my storyboard from AppDelegate. This viewController (chatVC) is behind a tabbar and a navigation controller and several other view controllers. See the image below. I know how to check the notification for tags and how to use launchOptions in AppDelegate to trigger the jump. But I am struggling with how to establish the context for that last view controller so that the user can use the back button all the way back to the tab bar controller.
I have read many SO answers, and tried many approaches but none seem to have the same embedding of a nav controller inside a tab bar controller. Here is my code in AppDelegate (after reading the tag in the notification):
if tag == "CS" {
// Set up a Chat View Controller.
if let chatVC = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: Constants.Storyboard.chatVC) as? NewChatViewController,
let tabBarVC = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: Constants.Storyboard.tabBarController) as? UITabBarController,
let csVC = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: Constants.Storyboard.csViewController) as? CustomerServiceViewController,
let helpVC = UIStoryboard(name:"Main", bundle: .main).instantiateViewController(withIdentifier: Constants.Storyboard.helpVC) as? HelpViewController
{
// Set the customer service document Id.
chatVC.cs = cs
// Make the tabBarVC the Root View Controller
self.window?.rootViewController = tabBarVC
self.window?.makeKeyAndVisible()
// Select the Favorites Index.
tabBarVC.selectedIndex = 0
// Push the Customer Service VC on top.
tabBarVC.show(csVC, sender: Any?.self)
// Push the Help VC on top of Customer Service VC.
csVC.show(helpVC, sender: Any?.self)
// Push the the chat detail page on top.
helpVC.show(chatVC, sender: Any?.self)
}
}
}
return true
What can I do to jump to the chatVC and set up the navigation context beneath it so the back button can be used?

Here is how you can do it:
guard let tabBarVC = UIApplication.shared.windows.filter( {$0.rootViewController is UITabBarController } ).first?.rootViewController as? UITabBarController else { return }
tabBarVC.selectedIndex = 0 //you can select another tab if needed
guard let chatVC = UIStoryboard(name: "Main", bundle: .main).instantiateViewController(withIdentifier: Constants.Storyboard.chatVC) as? NewChatViewController else { return }
if let navController = tabBarVC.viewControllers?[0] as? UINavigationController {
navController.pushViewController(chatVC, animated: true)
}
You can also pass objects from your notification to your chatVC here before pushing it, in case you need to do that.
Hope this works for you!

Related

NavigationController PushViewController does not show

I am trying to navigate to the ClassroomViewController once I receive the push notification. I have put a break point, and it hits all the lines, but it does not show the ClassroomViewController. I am wondering what I am missing in my current implementation.
I added identifier on the ClassroomViewController on storyboard.
AppDelegate
public var keyWindow: UIWindow? {
return UIApplication.shared.windows.first { $0.isKeyWindow }
}
Helper Method which does not work. I am wondering why the following approach does not work.
private func notificationToNavigate() {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let topViewController = appDelegate.keyWindow?.rootViewController
else { return }
let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let cVC = storyboard.instantiateViewController(withIdentifier:"ClassroomViewController") as! ClassroomViewController
topViewController.navigationController?.pushViewController(cVC, animated: true)
}
Helper Method which works
private func notificationToNavigate() {
guard let window = UIApplication.shared.keyWindow else { return }
let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let cVC = storyboard.instantiateViewController(withIdentifier:"ClassroomViewController") as! ClassroomViewController
window.rootViewController = UINavigationController(rootViewController: cVC)
window.makeKeyAndVisible()
}
If topViewController is indeed the key window's root view controller, then it cannot have a navigation controller; if pushing is possible at all, then it must be a navigation controller. So you would say
(topViewController as? navigationController)?.pushViewController...
On the other hand, if topViewController is not a navigation controller (so that the above fails), then pushing is simply impossible and you need to think of something else to do.

Navigation bar missing after tapping on remote notification

I have a side menu in app so on clicking the notification I navigate to view controller but cannot navigate to other VC's as it's not showing the navigation bar.
I've done this in my App Delegate and also have a navigation controller in my storyboard.
let sb = UIStoryboard(name: "Main", bundle: nil)
let otherVC = sb.instantiateViewController(withIdentifier: "messageview") as! MessageViewController
window?.rootViewController = otherVC;
func application(_ application: UIApplication,didReceiveRemoteNotification userInfo: [AnyHashable: Any],fetchCompletionHandler completionHandler:#escaping (UIBackgroundFetchResult) -> Void) {
let state = application.applicationState
if state == .inactive || state == .background {
let sb = UIStoryboard(name: "Main", bundle: nil)
let otherVC = sb.instantiateViewController(withIdentifier: "messageview") as! MessageViewController
window?.rootViewController = otherVC
print("background")
} else {
print("foreground")
}
}
Of course the navigation bar is removed; the view controller needs to be embedded in a navigation controller.
Try this:
let sb = UIStoryboard(name: "Main", bundle: nil)
let otherVC = sb.instantiateViewController(withIdentifier: "messageview") as! MessageViewController
// (actually, you don't need to cast to MessageViewController here;
// the returned reference already is guaranteed to at least be a
// UIViewController instance; you don't need more specificity in this
// case --i.e, embed in navigation)
let navigation = UINavigationController(rootViewController: otherVC)
window?.rootViewController = navigation
Note: This will present a fresh navigation with your view controller as the root. You will not be able to "navigate back" to any other screens that normally preceed it when navigating manually from your app's initial screen.
If you need to recreate a specific location within your navigation hierarchy, you will need to instantiate all previous view controllers up to the one you want to show, and set them instantly as the contents of the navigation stack.
Dummy example:
let first = UIStoryboard(name: "First", bundle: nil).instantiateInitialViewController()
let second = UIStoryboard(name: "Second", bundle: nil).instantiateInitialViewController()
let top = UIStoryboard(name: "Mail", bundle: nil).instantiateInitialViewController()
let vcs = [first, second, top]
let navigation = UINavigationController()
navigation.setViewController(vcs, animated: false)
window?.rootViewController = navigation
(this example assumes each VC lives in a separate storyboard, but you can also use instantiateViewController(withIdentifier:_) if that fits your setup)
Now, you can go back to "Second" and "First" using the navigation controller's back button.

Push to ViewController from AppDelegate with stack and navigation Bar

I'm developing a messaging app and I'm having trouble implementing push notification behaviour.
What I want is that when a user taps on the notification, the conversation of that group opens. But there has to be a Navigation bar to go back to the list of contacts.
I already know how to extract information from the notification to get the contact name. I'm trying this in the function didReceive on AppDelegate.
So far what I've tried is:
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ChatViewController") as! ChatViewController
vc.contactName = name as String
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = vc
self.window?.makeKeyandVisible()
This open the right conversation and the messages appear, but UI elements are no responsive (can't write when tapping textfield, cant navigate to gallery when tapping button, etc.) and also there is no Navigation bar.
So after doing some research, I've tried this:
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ChatViewController") as! ChatViewController
vc.contactName = name as String
let rootViewController = self.window?rootViewController as! UINavigationController
rootViewController. pushViewController(vc, animated: true)
This doesn't work. There is no error message but the screen is just white.
This is my Storyboard:
I'm using SWRevealViewController for the side menu and the messaging part has a tabBarController, with the contact list on the 2ยบ tab.
EDIT: With the second method, there is an error.
Could not cast value of type 'SWRevealViewController to UINavigationController'
I think you should go with NotificationCenter to do this.
When the user click on the notification you should post a NotificationCenter in the didReceive like this:
NotificationCenter.default.post(name: .onMessageRecieved, object: notificationObject)
And based on which view controller you're in you should add an observer there in the viewDidLoad() func so it understands a message has been recieved like this:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(MyViewController.onMessageRecieved(notification:)), name: .onMessageRecieved, object: nil)
}
And you should have a func (onMessageRecieved) in the view controller to do the navigation like this:
func onMessageRecieved(notification: NSNotification){
// open the conversation
let vc = self.storyboard?.instantiateViewController(withIdentifier: "ChatViewController") as! ChatViewController
vc.contactName = name as String
self.navigationController?.pushViewController(vc, animated: true)
}
This way you are doing the navigation from the view controller so the navigation will be there as well.
Could not cast the value of type 'SWRevealViewController to UINavigationController'
This issue is because of the "SWRevealViewController" is not a subclass of UINavigationController. So you can't cast it this way.
To solve the chat ViewController issue, you should modify your code like this.
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ChatViewController") as! ChatViewController
let nav = vc.navigationController
vc.contactName = name as String
let rootViewController = self.window?rootViewController as! UIViewController
rootViewController.presentViewController(nav, animated: true, completion: nil)
But the chat VC will be in the presenter mode. You should handle the dismiss.
You just try this and let me know is it working or not.

Show view controller from main storyboard

I have a case in one of my Swift files which runs the following method:
case .editProfile:
vc = PoiDetailViewController()
self.navigationController?.popViewController(vc,animated: true)
It doesn't seem to work when I run the code.
The view controller is located on the main storyboard and I would like PoiDetailViewController to display when this case is called.
If I understand it correctly, you should be able to achieve what you're looking for by present in your code. Like so:
case .editProfile:
let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyBoard.instantiateViewController(withIdentifier: "PoiDetailViewController") // or whatever identifier you have given it
self.present(vc, animated: true, completion: nil)
That should work, however, make sure you set a storyboard ID for your view controller in the identity inspector.
No segues are required.
To set up a storyboard ID click on the View Controller you would like to use (in this case, PoiDetailViewController) and click the identity inspector icon and set a storyboard ID where it asks. I have attached an image so you can see where it needs to go (in the field marked 'Storyboard ID')
Hope that helps.
As per my Understanding you need to pop to specific View Controller if it is in stack and if not then you need to push to that controller
var loginVCFound:Bool = false;
let navigationController : UINavigationController! = self.window!.rootViewController as! UINavigationController;
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil);
let viewControllers: [UIViewController] = navigationController.viewControllers
for aViewController in viewControllers {
if aViewController is LoginVC {// Pass the name of your controller here
loginVCFound = true;
navigationController.popToViewController(aViewController, animated: true)
break;
}
}
if !loginVCFound { // change the identifier and VC with yours
let objLoginVC = mainStoryboard.instantiateViewController(withIdentifier: "LoginVC") as! LoginVC
navigationController.pushViewController(objLoginVC, animated: true)
}

Set Initial view controller with tab bar and nav bar

I have a tab bar controller as my initial view controller in storyboard (with the arrow). However when a user signs in the first time, I'm trying to change the initial view controller programmatically in didFinishLaunchingWithOptions: to start out at a navigation controller to go through a couple VCs for them to enter signup information (I don't want a tab bar for this). Although I'm changing the initial VC in my app delegate, I'm getting a crash with Could not cast value of type 'UITabBarController' (0x1031374f8) to 'UINavigationController' (0x1031374a8). I was under the impression that the initial VC would be changed and not reference the Tab Bar Controller at all? Is there a way I can accomplish changing the initial VC without the Tab Bar Controller being referenced? Here is my code in didFinishLaunchingWithOptions:, any help is GREATLY appreciated!
if User.current() == nil {
let storyboard:UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let navigationControllerr:UINavigationController = storyboard.instantiateInitialViewController() as! UINavigationController
let initialViewController = storyboard.instantiateViewController(withIdentifier: "SplashPageVC")
navigationControllerr.viewControllers = [initialViewController]
self.window?.rootViewController = navigationControllerr
}
Why dont you try something like this.
if User.current() == nil {
let initialViewController = storyboard.instantiateViewController(withIdentifier: "SplashPageVC")
let navigationController = UINavigationController(rootViewController: initialViewController)
self.window?.rootViewController = navigationControllerr
}