I have tabbar controller having three tab, attached three navigation controller I want to go to second controller of navigation controller just like whatsapp. I handled successfully for background state, but for not running state. Below is my code in didfinishlaunch delegate method of uiapplication.
if (launchOptions != nil) {
// Launched from push notification
NSDictionary *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
[self performSelector:#selector(notificationObserverAfterDelay:) withObject:notification afterDelay:2.0];
}
-(void)notificationObserverAfterDelay:(NSDictionary *)userInfo
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"RefreshUIChatNotRunningState" object:self userInfo:userInfo];
}
Nazish Ali,
Sending out notification after delay of 2 seconds is really not a good idea.
Declare a method which accepts your userInfo dictionary as argument in your viewController having tab bar.
lets call it as,
YourViewController.h
-(void)appStartedFromPushNotification : (NSDictionary *)userInfo;
YourViewController.m
-(void)appStartedFromPushNotification : (NSDictionary *)userInfo {
//now the control has reached yourViewController with tab bar
//change the tab bar selection and perform segue and load the next viewController and use prepareforSgue to pass the data
}
Now why YourViewController only why not the actualViewController which anyway needs to be loaded ??? Reason you dont want mess with navigation stack manually. Because YourViewController is the child of UINavigationController and UINavigationController is your rootView controller YourViewController will be loaded automatically. So just access it and ask it to perform the tab change and load viewControllers so your navigation stack will continue to be stable :)
finally in Appdelegate,
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSDictionary *userInfo = [launchOptions valueForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
if (launchOptions) {
[self handleReceiveRemoteNotification: userInfo];
}
return YES;
}
Declare a method in AppDelegate.m and access the YourViewController instance and handover data to it thats all :)
- (void) handleReceiveRemoteNotification:(NSDictionary *)userInfo{
UINavigationController *rootViewController = (UINavigationController *)self.window.rootViewController;
for(UIViewController *viewController in rootViewController.viewControllers){
if([viewController isKindOfClass:[YourViewController class]]) {
[viewController appStartedFromPushNotification:userInfo];
}
}
That should do the job :) Happy coding :)
Related
I'm trying to implement circular dependency between my AppDelegate and my ViewController to call methods from my AppDelegate to my ViewController but it's not working.
See my code as follow:
AppDelegate.h:
#class ViewController;
#interface AppDelegate : UIResponder <UIApplicationDelegate>
#property (strong,nonatomic) ViewController *mainView;
#end;
AppDelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
return YES;
}
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
[self.mainView doSomething];
return YES;
}
ViewController.h:
#import <UIKit/UIKit.h>
#class AppDelegate;
#interface ViewController : UIViewController
#property (strong,nonatomic) AppDelegate *delegate;
-(void)doSomething;
#end;
ViewController.m:
- (void)viewDidLoad {
[super viewDidLoad];
self.delegate = (AppDelegate*)[[UIApplication sharedApplication]delegate];
}
- (void)doSomething
{
NSLog(#"doing something");
}
I don't have any errors or warnings but the method doSomething has never been call. Any of you knows why or what I'm doing wrong?
I'll really appreciate your help.
This has nothing to do with circular dependencies.
As you've been told, the method doSomething is never called because you are saying
[self.mainView doSomething];
...at a time when self.mainView has never been given a value. Merely declaring a property
#property (strong,nonatomic) ViewController *mainView;
...does not point the variable mainView at your actual ViewController instance; it is nil, and a message to nil generates no error and causes nothing at all to happen.
You could fix this by having the ViewController set a reference to itself by adding one line to your code:
- (void)viewDidLoad {
[super viewDidLoad];
self.delegate = (AppDelegate*)[[UIApplication sharedApplication]delegate];
self.delegate.mainView = self; // <--
}
But don't! The simple truth is that your entire approach here is wrong. There should be no need whatever to keep a reference to your ViewController inside your app delegate. Your app has, at every moment, a view controller hierarchy. The app delegate should know where the ViewController is within that hierarchy.
Here we are in your app delegate when a link message comes in:
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
At that moment, it is the app delegate's job to know where the ViewController is in the view controller hierarchy, and even to arrange the view controller hierarchy so that the ViewController's scene is showing if it wasn't already, in response to the link message.
How you do that depends on the structure of your view controller hierarchy. You start at the top of the hierarchy, which, for the app delegate, is [[self window] rootViewController], and work your way down to the existing view controller you want to talk to.
You have not told us your view controller hierarchy structure, so it's impossible to help in detail. But let's say, for example, that your app revolves around a navigation controller interface. Then, for the app delegate, [[self window] rootViewController] is the navigation controller, and so you can cast to that class: (UINavigationController*)[[self window] rootViewController]. Then, if the ViewController is the navigation controller's root view controller, you take its viewControllers[0] to reach it, and again you cast as needed:
UINavigationController* nav = (UINavigationController*)[[self window] rootViewController];
ViewController* vc = (ViewController*)nav.viewControllers[0];
Now you can send the doSomething message to vc. But that's just an illustration; the precise details will depend on where the ViewController really is, within the view controller hierarchy. And of course you might also want to pop view controllers so that the ViewController's scene is actually showing, since you likely cannot guarantee that it is showing at the time the link message comes in.
Another completely different way of handling this situation is to use the NSNotificationCenter to post a notification for which the ViewController instance has registered. That is often a solution when you do not know exactly where in the view controller hierarchy your view controller is. But in this situation, you should know that.
Try the below code :
AppDelegate.m :
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[[ViewController sharedInstance] doSomething];
return YES;
}
ViewController.h
+ (ViewController *)sharedInstance;
- (void)doSomething;
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
+ (ViewController *)sharedInstance {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
return [storyboard instantiateViewControllerWithIdentifier:#"ViewController"];
}
- (void)doSomething {
NSLog(#"ViewController is doing something");
}
Output :
ViewController is doing something
I don't have any errors or warnings but the method doSomething has never been call. Any of you knows why or what I'm doing wrong?
It happens because you haven't initialised an instance of ViewController. So, you have a nil at mainView. When you try to send a message "doSomething" to mainView you send message to nil. At Objective-C when you send a message to nil nothing is happens.
You should initialise an instance before you try to invoke the method. For example, at didFinishLaunchingWithOptions with such code:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.mainView = [ViewController new];
return YES;
}
It will works if you create views programatically. If you use a storyboards or xib you should use another methods.
Now you should see "doing something" at console when openURL is invoked.
BTW, you have a retain cycle between app delegate and view controller. So, your mainView will never release even if you make it explicitly nil. To avoid a retain cycle you should use attribute weak at ViewController.h:
#property (nonatomic, weak) AppDelegate *delegate;
You can do that by the below code but excuse me for doing it with Swift:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var mainVC: ViewController?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
mainVC = ViewController()
mainVC?.doSomething()
setupMainViewController()
return true
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
mainVC?.doSomething()
return true
}
func setupMainViewController(){
guard window != nil else{
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = mainVC
window?.makeKeyAndVisible()
return
}
window?.rootViewController = mainVC
}
and the MainViewController will be:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
doSomething()
}
func doSomething(){
print("do something with \(self)")
}
I appreciate all the other answers, I come here with a different approach. Generally, I used in my projects.
NSNotificationCenter defaultCenter Please check the code below.
Viewcontroller.m
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//self.delegate = (AppDelegate*)[[UIApplication sharedApplication]delegate];
}
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// HERE REGISTER NOTIFICATION WHEN SCREEN IS APPEAR
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(doSomething:)
name:#"TestNotification"
object:nil];
}
// HERE ADDS NOTIFICAIOTN AS PARAMETERS
- (void)doSomething :(NSNotification *) notification
{
NSLog(#"doing something");
// IF YOU PASSED VALUE WITH THE NOTIFICAIONT THEN IT WILL SHOW HERE
NSDictionary *userInfo = notification.userInfo;
id myObject = [userInfo objectForKey:#"someKey"];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// HERE REMOVE NOTIFICATION OBSERVER
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
AppDelegate.m :
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
return YES;
}
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
//WITHOUT SENDING ANY DATA
[[NSNotificationCenter defaultCenter]
postNotificationName:#"TestNotification"
object:self];
// IF YOU WANT TO SEND ANY INFROMATION THEN PLEASE USE THIS METHODS
NSDictionary *userInfo =
[NSDictionary dictionaryWithObject:myObject forKey:#"someKey"];
[[NSNotificationCenter defaultCenter] postNotificationName:
#"TestNotification" object:nil userInfo:userInfo];
return YES;
}
Here you do not need to check the visible view controller and no allocation required. if your screen is in the top then your methods are called otherwise not.
Good luck! :-)
I am trying to implement background refresh in my app. I have a method refreshMessages that is inside one of my UIViewControllers (I am using a UINavigationController). I need to access this controller from AppDelegate.m.
Usually I would do something like this:
UINavigationController *navigationController = [self.window.rootViewController navigationController];
to get the navigation controller and I can then access them from there. But surely this won't work now. The app is in the BACKGROUND and no window is showing. Any thoughts on how to get around this? Thanks!
Here is my background refresh:
-(void)application:(UIApplication *)application
performFetchWithCompletionHandler:
(void (^)(UIBackgroundFetchResult))completionHandler {
NSLog(#"Doing the background refresh");
UINavigationController *navigationController = [self.window.rootViewController navigationController];
NSLog(#"Number of controllers is %d", [[navigationController viewControllers] count]);
//There are zero controllers?!!! Must be because no window is showing
completionHandler(UIBackgroundFetchResultNewData);
}
Yes, you are right. The window will be nil since it will no longer be in the view hierarchy.
A cheeky solution would be to hold the reference of the view controller in the AppDelegate by creating a property in it. You can assign this when the application goes to the background by subscribing to the relevant notification in the view controller.
Edit: Code added
In your AppDelegate.h
#property (nonatomic, weak) YourViewController *yourViewController;
In YourViewController.m, somewhere in viewDidLoad
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
appWillResignActive's implementation
-(void)appWillResignActive:(id)sender {
YourAppDelegate *delegate = [UIApplication sharedApplication].delegate;
delegate.yourViewController = self;
}
In your completion handler
[self.yourViewController refreshMessages]
Also, make sure you remove the observer once the view controller is deallocated.
I want to display a specific UIViewController when a push notification is received and the user tap on that notification. I made some research and tried a few things without success.
I tried the following code, it works on UIAlertController but it didn't display the specific UIViewController I wanted. Please help me out. I am using Xcode 6.3.
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSDictionary *userInfo = [launchOptions valueForKey:#"UIApplicationLaunchOptionsRemoteNotificationKey"];
NSDictionary *apsInfo = [userInfo objectForKey:#"aps"];
if(apsInfo) {
UIStoryboard *myStoryBoard = [UIStoryboard storyboardWithName:#"Main" bundle:[NSBundle mainBundle]];
notificationViewController *nvc = [myStoryBoard instantiateViewControllerWithIdentifier:#"notificationViewController"];
[self.window.rootViewController presentViewController:nvc animated:YES completion:nil];
}
return YES;
}
Thanks in advance
You could create a 'Router' object that handle the notification payload, and pass that object from ViewController to ViewController, building the navigation stack step by step.
Here is a link to a sample project I made to illustrate that concept. For the sake of the demo, the 'Router' is built following a call to buildNavigationStack in application:application handleOpenURL:, but you could easily change that to use the userInfo dictionary received in application:didReceiveRemoteNotification:fetchCompletionHandler:.
For this need, I create a UIViewController variable called currentViewController in my AppDelegate class or my Singleton class if it exists in the project.
For every VC created, I add the following line inside the viewWillAppear and viewDidAppear:
[(myAppDelegate *)[[UIApplication sharedApplication] delegate] setCurrentViewController:self];
This way the VC being shown on the screen is always kept in an ivar.
So in - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo or where ever you are presenting the alertViewController you can present or push your new VC easily.
[self.currentViewController presentViewController:vc animated:YES completion:nil];
Full Detail Solution:
In this approach the critical and tricky part is setting the currentViewController in a singleton class. Rest will be very straight forward.
Create Singleton Class:
//CapsHelper.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#interface CAPSHelper : NSObject
{
}
#property (nonatomic, strong) UIViewController * currentViewController;
+ (instancetype)sharedHelpers;
//CapsHelper.m
#import "CAPSHelper.h"
#interface CAPSHelper ()
#end
#implementation CAPSHelper
#synthesize currentViewController;
#pragma mark Singleton Methods
+ (instancetype) sharedHelpers
{
static CAPSHelper *sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
});
return sharedMyManager;
}
Lets say you have around 10 viewcontrollers in your app called VC1, VC2, .., VC10 etc. In each VC's viewWillAppear:animated:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[[CAPSHelper sharedHelpers] setCurrentViewController:self]
}
Or if this seems like lots of repetition, you can create a new class and override viewWillAppear:animated: and you wont have trouble trying to remember adding above to new VC's you will create in the future.
At this moment we are all set, every time a VC is being presented to the screen, we are holding it in a variable called currentViewController, and it is accessible from everywhere by importing #import "CAPSHelper.h"
//AppDelegate.m
#import "AppDelegate.h"
#import "CAPSHelper.h"
#interface AppDelegate ()
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSDictionary *userInfo = [launchOptions valueForKey:#"UIApplicationLaunchOptionsRemoteNotificationKey"];
NSDictionary *apsInfo = [userInfo objectForKey:#"aps"];
if(apsInfo)
{
if([[CAPSHelper sharedHelpers] currentViewController])
{
//UIViewController * pushNotificationViewController = [[UIViewController alloc] ..... This is your specific VC which you want to present when a PN is recieved..
[[[CAPSHelper sharedHelpers] currentViewController] presentViewController:pushNotificationViewController animated:YES completion:nil];
}
}
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
NSDictionary *apsInfo = [userInfo objectForKey:#"aps"];
if(apsInfo)
{
if([[CAPSHelper sharedHelpers] currentViewController])
{
//UIViewController * pushNotificationViewController = [[UIViewController alloc] ..... This is your specific VC which you want to present when a PN is recieved..
[[[CAPSHelper sharedHelpers] currentViewController] presentViewController:pushNotificationViewController animated:YES completion:nil];
}
}
}
My NSNotification is delayed.
In myVC, I have two buttons, buttonA and buttonB. Each one is linked to their respective pdfs, pdfA and pdfB. There is a button Push, which is pressed after A or B is pressed. Push brings the user to the RVC where there is a UIWebView.
I want it so that by pushing either A or B, the UIWebView will display the respective pdf file. To debug this, I set it so that instead of changing the pdf, it will display text using NSLog. However, it doesn't work until after go back to myVC from the RVC by pressing a different button.
in myVC.m file:
- (IBAction)open_pictures_A:(id)sender
{
//do some button alert popup action/whatever button does
RootViewController *dataObject = [RootViewController new];
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:dataObject
forKey:#"buttonA"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"MyNotification"
object:nil
userInfo:userInfo];
}
And there is one like that for buttonB, but forKey would be "buttonB"
in the viewDidLoad for myVC.m I have
[[NSNotificationCenter defaultCenter] postNotificationName:#"MyNotification"
object:nil];
in my RVC,
RVC.h I have
#property (nonatomic, strong) NSString *property1;
And in the RVC.m I have
- (void)viewDidLoad
{
// other stuff
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(eventListenerDidReceiveNotification:)
name:#"MyNotification"
object:nil];
}
- (void)eventListenerDidReceiveNotification:(NSNotification *)notif
{
if ([[notif name] isEqualToString:#"MyNotification"])
{
NSLog(#"Successfully received the notification from buttonB!");
NSDictionary *userInfo = notif.userInfo;
RootViewController *dataObject = [userInfo objectForKey:#"buttonB"];
// Your response to the notification should be placed here
NSLog(#"dataObject.property1 -> %#", dataObject.property1);
}
}
However, the log entry only shows up when I press a button to exit out of the RVC back to myVC
Here is the code I use to go from mVC to RVC
-(IBAction)goToRVC{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
RootViewController *RVC = [storyboard instantiateViewControllerWithIdentifier:#"Root"];
[UIApplication sharedApplication].delegate.window.RootViewController = RVC;
}
Then from RVC to myVC
-(IBAction)backtomyVC{
[[[UIApplication sharedApplication] delegate] performSelector:#selector(myVC)];
[self disconnect];
}
Why is my notification action being delayed?
It's not because of delay.
When you press your button A or B you create new RVC object and you post the notification however you don't present/push RVC view controller (you just initialise it) so the RVC hasn't fired viewDidLoad method and it hasn't register itself as an observer.
After that you call goToRVC method where you create RVC object and you add it to the view hierarchy so the viewDidLoad method is call and the object register itself as an observer.
When you go back to mVC the RVC is not deallocated yet so it receive the notification and you can see the log.
Hope it's clear.
Why do I want to use presentModalViewController in AppDelegate?
- Processing didReceiveLocalNotification, so I can launch a seperate modalView on top of my app to process the notification
What does my view architecture look like?
- Using storyboards
- MainStoryBoard: ->TabBarController->NavigationController
What's happening?
- Nothing, that's the problem :-D
- When I press the action button from the UILocalNotification, the app opens, but just shows the last open view from the tabbarcontroller.
As you can see below my last effort was to present the modalViewController on top of that current view, like so:
[self.window.rootViewController.tabBarController.selectedViewController.navigationController presentModalViewController:navigationController animated:YES];
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
UIApplicationState state = [application applicationState];
if (state == UIApplicationStateInactive) {
// Application was in the background when notification was delivered.
NSLog(#"Received notification while in the background");
}
else {
NSLog(#"Received notification while running.");
}
MedicationReminderViewController *controller = [[UIStoryboard storyboardWithName:#"ModalStoryBoard" bundle:nil] instantiateViewControllerWithIdentifier:#"MedicationReminderVC"];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:controller];
[self.window.rootViewController.tabBarController.selectedViewController.navigationController presentModalViewController:navigationController animated:YES];
}
Update
Seems that this is nil:
self.window.rootViewController.tabBarController.selectedViewController.navigationController
Solution
[self.window.rootViewController presentModalViewController:navigationController animated:YES];
Try this :
[self.window.rootViewController presentModalViewController:controller
animated:YES];
Have you tried the following?
[self.window.rootViewController.tabBarController.selectedViewController presentModalViewController:navigationController animated:YES];
That said, even if this works, I would really urge you to reconsider your design choices to avoid having to do this. Traversing the navigation stack in this way to access stuff can get very messy and I'd strongly advise against it.