I'm using modal segue (without navigation controller) to move between viewController A and viewController B like so:
viewA *first = [self.storyboard instantiateViewControllerWithIdentifier:#"viewA"];
[self presentViewController:first animated:YES completion:nil];
And to move back :
[self dismissViewControllerAnimated:YES completion:nil];
Now, I want to know from the AppDelegate whether A or B is the current view right now.
The problem is when I'm checking
[(AppDelegate *)[[UIApplication sharedApplication] delegate] window]
the answer is always view A - the first one.
I've tried to set the current view every time I'm using modal segue like so:
viewA *first = [self.storyboard instantiateViewControllerWithIdentifier:#"viewA"];
[self presentViewController:first animated:YES completion:^
{
[[(AppDelegate *)[[UIApplication sharedApplication] delegate] window] setRootViewController:first];
}];
But it cause a few bugs (like unable to use "dismissViewControllerAnimated"),and it's impossible to work like that in every segue in a big project with many segues.
How should I work with that? And how should I detect the current view in more appropriate way?
As was answered here
UIWindow *topWindow = [[[UIApplication sharedApplication].windows sortedArrayUsingComparator:^NSComparisonResult(UIWindow *win1, UIWindow *win2) {
return win1.windowLevel - win2.windowLevel;
}] lastObject];
UIView *topView = [[topWindow subviews] lastObject];
However, doing this logic here sounds like bad architecture. What is the reason for you needing to know which view is currently presented inside of your AppDelegate?
Edit It seems like you want to respond to the applicationWillResignActive event from your view controller. Use something like this in the your game view controller.
- (void) applicationWillResign {
NSLog(#"About to lose focus");
}
-(void) viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(applicationWillResign)
name:UIApplicationWillResignActiveNotification
object:NULL];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[[NSNotificationCenter defaultCenter]
removeObserver:self];
}
The appDelegate's window won't be equal to either view controller (viewControllerA or viewControllerB). You can ask the window for it's root view controller...
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
if (appDelegate.window.rootViewController == viewControllerA) {
// always true if you always start the app with viewControllerA
... and you can ask any view controller for the view controller it presented...
if (appDelegate.window.rootViewController.presentedViewController == viewControllerB) {
// will be true if viewControllerA has presented viewControllerB
But this is a tricky game. If, for example, viewControllerB presents some other viewControllerC, the condition above will continue to be true.
See the #Eric answer here (not the accepted answer) for a way to find the topmost vc in general.
Related
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 will try to explain this best i can. Okay i am using SpriteKit in xcode5. Inside Myscene.m i have a method called:
-(void)presentViewController
{
UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
UIViewController *vc = [mainStoryboard instantiateViewControllerWithIdentifier:#"MyView"];
[self.view.window.rootViewController presentViewController:vc animated:YES completion:nil];
}
Then in my NSTimeintervalUpdate method i have a code that says
if(score >= 3)
{
[self presentViewController]
}
All this code brings up the view controller i want from the storyboard like it should with no problems but in this view controller it has a button that links up back to the game. When you click the button it goes right back to the game like it should. Well during the game instead of going back to my ViewController at a score of "3" like it did the first time, it just continues to count up and provides the following error message in the log:
2014-07-12 22:40:27.710 tests[337:60b] Warning: Attempt to present <ViewController2: 0xc354e40> on <ViewController: 0x9960b80> whose view is not in the window hierarchy!
My intention is to have my game (Myscene.m) and when the game is over is to have a game over screen (ViewController). And then from this view controller i want it to have a play again button that is linked back to Myscene.m(which i simply did by making button and control and drag to the view controller that handles my SKScene) and continues to repeat process but it will only do this process once and then it fails to loop back around and instead presents the above error.
Any help appreciated thanks!
Don't present View Controller from SKScene directly. Use NSNotificationCenter to tell parent View Controller to present another View Controller:
In GameSceneViewController.m:
#interface GameSceneViewController
#property (nonatomic) int score;
#end
#implementation GameSceneViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(presentMyViewController:) name:#"presentMyViewController" object:nil];
}
- (void)presentMyViewController:(NSNotification *)notification {
self.score = notification.userInfo[#"score"];
[self performSegueWithIdentifier:(segue identifier) sender:nil];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// Make sure your segue name in storyboard is the same as this line
if ([[segue identifier] isEqualToString:(segue identifier])
{
// Get reference to the destination view controller
MyViewController *vc = [segue destinationViewController];
// Pass any objects to the view controller here, like...
vc.score = self.score;
}
}
#end
Then in GameScene.m call:
[[NSNotificationCenter defaultCenter] postNotificationName:#"presentMyViewController" object:nil userInfo:#{"score" : self.score}];
i have three controller and i wanna kown the controller is a push or pop
A controller:
{
if(!b)
b = [B alloc] init];
[self.navigationController pushViewController:b animated:YES];
}
B controller:
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
//I want here to judge, from the "A" push over, or to return from the "C" "pop"
//if it is push from A
//dosomething.....
//if it is pop from C
//dosomething
}
-(void)testAction:(id)sender
{
C *c = [[C alloc] init];
[self.navigationController pushViewController:b animated:YES];
[c release];
}
C controller:
{
[self.navigationController popViewControllerAnimated:YES];
}
thanks.
Have a look at the UIViewController method, isMovingToParentViewController. This will return YES if the view controller is being shown because it was pushed, but NO if it is shown because another view controller was popped off the stack.
-(void)viewDidAppear:(BOOL)animated { //Code in view controller B
[super viewDidAppear:animated];
NSLog(#"isMovingToParentViewController: %d",self.isMovingToParentViewController);
// this will log 1 if pushing from A but 0 if C is popped
}
EDIT :
Add UINavigationControllerDelegate in .h file
Also do this:
[self.yournavController setDelegate:self];
Method below is navigation controller delegate which is called when the navigation controller shows a new top view controller via a push, pop or setting of the view controller stack.
Add this method
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
}
Hmm I think for that you need to track a global variable that knows either it is push from A or pop from C. What I would do is:
Declare a BOOL variable isPush in appDelegate or some external .h file and synthesize it.
When you are going from A to B i.e. it is a push, make it equal "YES" in A.
yourAppDelegate *myDelegate = (yourAppDelegate*) [[UIApplication SharedApplication] delegate];
myDelegate.isPush = YES;
Similarly before popping from C, make value of isPush = NO;
In B's viewDidLoad, see the value of variable.
yourAppDelegate *myDelegate = (yourAppDelegate*) [[UIApplication SharedApplication] delegate];
if(myDelegate.isPush)
//means A was pushed
else
//means C was popped
It it possible to override the default view controller from a storyboard to show a different controller instead? This would all happen in the AppDelegate of course.
#Martol1ni I wanted to use your answer, but I also wanted to stay away from unnecessary storyboard clutter so I tweaked your code a bit. However I did give you a +1 for your inspiring answer.
I put all of the following on the default controller.
- (void)gotoScreen:(NSString *)theScreen
{
AppDelegate *app = (AppDelegate *)[[UIApplication sharedApplication] delegate];
UIViewController *screen = [self.storyboard instantiateViewControllerWithIdentifier:theScreen];
[app.window setRootViewController:screen];
}
And then where the logic happens I'll call the following as needed.
if(myBool == YES) {
[self gotoScreen:#"theIdentifier"];
}
I would definately embed a rootView in a UINavigationController, so you have not two, but three views. The one is never launched, just in control of all the otherones. Then implement the methods in it like this:
- (void) decideViewController {
NSString * result;
if (myBool) {
result = #"yourIdentifier";
}
else {
result = #"yourOtherIdentifier";
}
self.navigationController.navigationBarHidden = YES; // Assuming you don't want a navigationbar
UIViewController *screen = [self.storyboard instantiateViewControllerWithIdentifier:#"view1ident"];
[self.navigationController pushViewController:screen animated:NO]; // so it looks like it's the first view to get loaded
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self decideViewController];
}
It never looks like the first view is loaded. If you are using NIBS, you can do everything from AppDelegate though...
I have a view controller subclass, SignInViewController, used for sign in that might be needed at any time. Rather than have every view controller in my app listen for the notification that sign in is needed, I'd rather have the app delegate do it.
But how do I trigger it from my app delegate?
Do I put the SignInViewController in my main storyboard? If so, how do I access my storyboard from my app delegate? Or is some other approach better?
You can always reference to your app delegate through the UIApplication singleton.
From there you can always get your root view controller.
With your root view controller you can get a reference to the storyboard.
Once you have your story board all you do is instantiate an instance of the view controller you want.
Present it.
AppDelegate* appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
MainViewController *mvc = (MainViewController *)appDelegate.window.rootViewController;
LoginViewController *lvc = [mvc.storyboard instantiateViewControllerWithIdentifier:#"LoginViewController"];
[currentVC presentModalViewController:lvc animated:YES];
There may be a more direct way of getting a reference to your storyboard but this will almost always get it for you.
To show a view controller from anywhere (including the app delegate), I have used this code with success in iOS 8+ (I'm not sure about earlier compatibility). It will present from the modal view if there is one.
YOURAppDelegate *appDelegate = (YOURAppDelegate *)[[UIApplication sharedApplication] delegate];
UINavigationController *rootNavC = (UINavigationController *)appDelegate.window.rootViewController;
UIViewController *topVC = rootNavC.topViewController;
UIViewController *myNewVC = [rootNavC.storyboard instantiateViewControllerWithIdentifier:<YOUR STORYBOARD ID>];
if (topVC.presentedViewController)
{
if ([topVC.presentedViewController class] == [UINavigationController class])
{
dispatch_async(dispatch_get_main_queue(), ^{
[((UINavigationController*)topVC.presentedViewController) pushViewController:myNewVC
animated:YES];
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[topVC.presentedViewController.navigationController pushViewController:myNewVC
animated:animated];
});
}
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[rootNavC pushViewController:myNewVC
animated:animated];
});
}
I usually wrap this in a convenience method and pass in my instantiated view controller. To OP - you would create SignInViewController in the storyboard, assign it a unique storyboard ID, and then substitute that ID in the above code.