Call ViewControllers Method or Segue from App Delegate (Swift) - ios

I have created an app that uses storyboard and have successfully created a tableview and detail page which all works.
I would like it so that users swiping the localNotifications can be sent to the correct detail page within the app.
It appears that I can call functions from the ViewController but whenever they refer to themselves to update any details or perform a segue the app crashes.
The code i have in my ViewController is as follows:
func handleMessageNotification (messageID:String)
{
// this function should act as a conduit for the call from the delegate
println("notif messageID: \(messageID)");
self.performSegueWithIdentifier("showMessageDetail", sender: self);
self.messageView.reloadData();
}
and this is called from my appDelegate
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
if (application.applicationState == UIApplicationState.Background || application.applicationState == UIApplicationState.Inactive)
{
var rootViewController = self.window!.rootViewController
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var setViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController") as ViewController
rootViewController?.navigationController?.popToViewController(setViewController, animated: false)
setViewController.handleMessageNotification(messageID);
}
}
the println works correctly but performSegue fails (fatal error: unexpectedly found nil while unwrapping an Optional value) and the messageView.reload() fails (same error).
How do I get a notification to fire the app to the correct place upon opening?
This solution 'Get Instance Of ViewController From AppDelegate In Swift' uses much of the same but mine will not allow access to anything with the ViewController.
======= Update - Solution =======
For anyone else with this issue. Following on from what Gabuh had suggested below; the full solution for me was to do the following:
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
if (application.applicationState == UIApplicationState.Background || application.applicationState == UIApplicationState.Inactive)
{
let navigationController = application.windows[0].rootViewController as UINavigationController;
navigationController.popToRootViewControllerAnimated(false); // I need to push back to root before performing segue
let rootViewController = navigationController.visibleViewController;
rootViewController.performSegueWithIdentifier("showMessageDetail", sender: self);
}
}
this also allows me to make calls on functions in the view such as in the original example e.g.
rootViewController.handleMessageNotificaion(messageID);
or
rootViewController.messageView.reloadData();

You are creating an instance of a new ViewController. And you are trying to call a function on that ViewController, that is trying to perform a segue, when the ViewController is not even displayed. I think that's not going to work...
If you want to perform a segue from your detail view, you have to access to the current instantiated view Controller and perform the segue from it.
If you want to know how to get the instance of the UIViewController that is being displayed, in this post there are several ways that show you how to do it:
Get the current view controller from the app delegate
In pseudo-code, in your appDelegate:
1- get current UIViewController instance (not new one)
2- perfom segue from that view controller (or present modally or whatever transition you want)

In Swift 3:
guard let rvc = self.window?.rootViewController as? VCName else { return }
rvc.methodInYourVC()
The above assumes
That you don't want to create a whole new instance of VCName, just want a reference to it
That you know the VC you want to reference is your root VC

Related

How to check in AppDelegate if a particular ViewController is currently open

I am trying to prevent a push notification show on the app home screen when a certain userMessagesViewController is currently open.
I don't want users receiving a push notification if this specific viewController is open. My function that sends the push notification is in the appDelegate. How can I check. Here is my implementation so far.
let messagesVC = UserMessageViewController()
if messagesVC.view.window != nil {
print("Messages viewcontroller is visible and open")
} else {
print("Messages viewcontroller isnt visible and not open")
}
By initiating messagesVC, you're creating a brand new UserMessageViewController that won't have been presented yet. The particular instance of the controller you want will already be instantiated, so you must find it using the view controller hierarchy.
The AppDelegate gives you access to the rootViewController of your application which will be the very first controller you have in your storyboard. From this controller, you can make your way through the child view controllers in search of a UserMessageViewController.
Here is an extension that will start at the rootViewController and bubble its way up until it reaches the top of the view controller hierarchy stack.
extension UIApplication {
func topViewController(_ base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
switch (base) {
case let controller as UINavigationController:
return topViewController(controller.visibleViewController)
case let controller as UITabBarController:
return controller.selectedViewController.flatMap { topViewController($0) } ?? base
default:
return base?.presentedViewController.flatMap { topViewController($0) } ?? base
}
}
}
Create a new file called UIApplication+TopViewController.swift and paste in the above extension. Then inside AppDelegate, you will be able to get the current view controller that is being presented using UIApplication.shared.topViewController():
if let messagesVC = UIApplication.shared.topViewController() as? UserMessageViewController {
print("Messages viewcontroller is visible and open")
} else {
print("Messages viewcontroller isnt visible and not open")
}
By casting the top view controller to UserMessageViewController, we can determine whether or not the notification should be presented.
This should work for you:
if messagesVC.viewIfLoaded?.window != nil {
// viewController is visible, handle notification silently.
}
Your appDelegate will have a reference to the VC. It should probably be a property of the delegate.

IOS - How to jump between viewControllers in different scenarios

I have 5 viewControllers(A,B,C,D,E), all these viewControllers are programmatically connected, and i could push and pop between them successfully.
Using:
navigationController?.pushViewController(loginVc, animated: true)
_ = navigationController?.popViewController(animated: true)
NOTE: ViewController A appears first as a default initial viewController.
Now, what i want is, when the user installs the App, only for the first time the viewController A must be shown as the initial viewController, rest of the times when the App is opened, the viewController D must be the initial viewController and from there i should be able to jump between previous viewControllers. How can i implement this. Im using Xcode 8.2, Swift 3.0
Thanks in advance.
In order to do that, you simply could add a boolean to your NSUserDefaults, using the following code:
let defaults = UserDefaults.standard
if (!defaults.bool(forKey: "firstTime")) { //will be false if does not exist yet
navigationController?.pushViewController(yourDesiredVC, animated: true) //push desired vc
defaults.set(true, forKey: "firstTime") //set the key so it never executes again
} else {
navigationController?.pushViewController(yourDesiredVC, animated: true) //push the other vc
}
Your question doesn't really say very much without some code, but one suggestion (given the current quality of the question) would be to use UserDefaults. Add your current version of the app to a key called e.g. LatestVersion. Compare it at launch with the apps current version, if they don't match, show ViewControllerA, if not show ViewControllerB.
Another way is just saving launchedForFirstTime. If its not set show ViewControllerA, however the above would take in account future versions of the app where you might want to show that view as well.
You can keep a value in UserDefaults to keep track of the returning users and check if it's there:
if let returning :Bool = UserDefaults.standard.bool(forKey: "initial_controller_shown") {
//returning user flow
} else {
//new user flow
}
A common place to check for this is in the applicationDidBecomeActive or didFinishLaunchingWithOptions
when first time your app launched then use a flag and store some value in it so that next time when your app run then you can check that whether user visit the app for the first time or not .. Now after that go to appDelegate and paste the following code in DidFinishLaunchingWithOption...
if yourFlag == true
{
let mainStoryboard: UIStoryboard = UIStoryboard(name: "MainStoreyBoard", bundle: nil)
let controller = mainStoryboard.instantiateViewController(withIdentifier: "StoreyBoardIdofYourViewController") as! UINavigationController
self.window?.rootViewController = controller
}
This will launch D viewcontroller .....
You can check it in your app delegate.m file whether the app installed first time ViewController A will appear as a initial view controller else view controller D. Check it in the following function:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//check the initial view controller here//
return true
}
Swift 3:
let launchBefore = UserDefaults.standard.bool(forKey: "launchBefore")
if launchBefore {
print("Not first launch.") } else {
print("First launch, setting UserDefault.")
UserDefaults.standard.set(true, forKey: "launchBefore") }
You are using one Navigation Controller so it will be hard to implement your behavior. It will be much easier for you to use separate View Controllers for View A and D and call them using:
present(vc, animated: true)
and dismiss calling:
dismiss(animated: true)

How to hide a Storyboard/ViewControllers forever?

I am working on an app that has two Storyboards: One for my app's onboarding controllers and the other for my main app. My onboarding view controller has a skip button that when pressed directs the user to the main storyboard. I only want to show the onboarding view as long as the user hasn't hit the skip button. Once the skip button is hit, the corresponding storyboard should disappear forever.
I thought I could fix this problem by only making showing onboarding storyboard when the app is first opened, and I found some code online that seemed helpful, however it doesn't work...here is the code that is in my app's AppDelegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
FIRApp.configure()
Fabric.with([Crashlytics.self])
let defaults = NSUserDefaults.standardUserDefaults()
if (defaults.objectForKey("userAsLoggedInBefore") != nil) {
print("Functions")
//here you insert the code to go to the main screen
var mainView: UIStoryboard!
mainView = UIStoryboard(name: "Functions", bundle: nil)
let viewcontroller : UIViewController = mainView.instantiateViewControllerWithIdentifier("functions") as UIViewController
self.window!.rootViewController = viewcontroller
} else {
//this means the user hasn't logged in before and you can redirect him to the onboarding page
print("Onboarding")
var mainView: UIStoryboard!
mainView = UIStoryboard(name: "Main", bundle: nil)
let viewcontroller : UIViewController = mainView.instantiateViewControllerWithIdentifier("onboarding") as UIViewController
self.window!.rootViewController = viewcontroller
}
}
Note in my code "Main" is the onboarding storyboard, while "Functions" is my main app code.
However this code is not working as intended as my same problem still arises. If anybody could take a look at my code and see if I am doing anything wrong, that would be greatly appreciated.
Thanks in advance!
You are never writing to NSUser defaults. So it will stay nil..
Your missing some line like this:
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setValue("Did watch", forKey: "userAsLoggedInBefore")
Put it at the end of if - else statement

Segue to a view controller from appDelegate

I have an app set up to receive push notifications. When a user receives a notification, I have a callback in my appDelegate. I need to be able to segue from here if the app was inactive and the user clicks on the notification from the panel on the device.
The flow of the app is a login view controller (which gets skipped if the loginBool is true) which leads to a tab controller. On the tab controller I have 3 places where I can segue to the same viewController with an id of "FeedDetailedController".
It is the FeedDetailedController I need to segue to and pass in a variable which I am receiving in my notification. This controller can be accessed from 3 different places 2 of which are tabs with table views, when you click on a row, it passes in a variable and performs a segue. I need to miic this from my app delegate i.e. pass in data from the notification, like what I am doing with the row.
Attempt so far:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
println("received a notification")
PFPush.handlePush(userInfo)
if application.applicationState == UIApplicationState.Inactive {
println("in the notification if with \(userInfo)")
if let info = userInfo["custom"] as? Dictionary<String, AnyObject> {
if let reportId = info["reportId"] as? String {
println("\nFrom APS-dictionary with key \"type\": \(reportId)")
//pass in reportId to the viewcontroller somehow
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("NewFeedDetailedController") as! UIViewController
let navigationController = UINavigationController(rootViewController: vc)
self.window?.rootViewController?.presentViewController(navigationController, animated: true, completion: nil)
}
}
PFAnalytics.trackAppOpenedWithRemoteNotificationPayload(userInfo)
}
else{
println("in the notification else")
//this is when the app is active, do I need to detect which view controller I am currently on before I can seg???
}
}
The current code gives the following message:
Warning: Attempt to present <UINavigationController: 0x12ed763d0> on <UINavigationController: 0x12ed11ce0> whose view is not in the window hierarchy!
Which makes sense but I don't know how I am supposed to get the hierarchy right coming from appDelegate code
Would it be possible to check for that notification in the initial view controller of your app? Like pass it the notification as a boolean? You could look somewhere like viewWillAppear: and check for it - if it is there control the segue from that view instead of trying to do it in the appDelegate itself?
That way it would appear to go directly into the new view, but really be passing through a view controller that has the hierarchy loaded into it?

ViewController deinit after performing segue using Notification

I'm basically receiving a remote notification and I want to redirect my user to the correct VC as soon as he clicks the notification.
I'm doing it using NSNotificationCenter to perform a segue from my rootVC, leading the user to the correct VC.
NSNotificationCenter.defaultCenter().addObserver(self, selector:
"chooseCorrectVC:", name:chatNotificationKey, object: nil)
Since the observer was previously loaded, my chooseCorrectVC function is called first, so this is my "Init/Deinit" Log. I consider Init whenever viewDidLoad() is called.
rootVC INIT
SecondVC DEINIT
rootVC DEINIT
func chooseCorrectVC(notification:NSNotification){
self.performSegueWithIdentifier("chatSegue", sender: notification)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
The issue is: the VC that is called with chatSegue does not get initialized and goes straight to deinit. I'm not sure why it's happening, maybe I'm not removing the observer correctly.
Any suggestions?
If you are receiving the remote notification, I suggest you to just handle the notification at AppDelegate.swift at method:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]){
// Here you can define view controller and manage it.
println("Received: \(userInfo)")
// Make root view controller first as per your need as HomeViewController in following
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var mainTabBarController = mainStoryboard.instantiateViewControllerWithIdentifier("MainTabBarController") as! MainTabBarController
var notificationNavController : UINavigationController = mainTabBarController.viewControllers?.first as! UINavigationController
var homeViewController : HomeViewController = notificationNavController.viewControllers.first as! HomeViewController
homeViewController.isNotified = true
let nav = UINavigationController(rootViewController: mainTabBarController)
self.window!.rootViewController = nav
nav.setNavigationBarHidden(true, animated: false)
self.window?.makeKeyAndVisible()
}
You can manage another view controller to be push on viewdidload of homeviewcontroller via setting flag here. On view did load
if isNotified == true {
// Push another view controller
}

Resources