Displaying Login View on app resume - objective-c

What is the best way to display a view (in my case a login screen) on app resume. From looking around, I've been playing with the applicationDidBecomeActive event in my AppDelegate, but I cannot seem to get my head around how to properly display a view from here.
I've tried to grab the current window by using self.window and/or it's subviews, but from the AppDelegate self.window is nil.
So far this application seems to be wired up correctly, but I am baffled by two things.
A) why is self.window nil from within my AppDelegate's applicationDidBecomeActive event handler.
B) what is the correct/normal way of display a login view (or the like) on application resume.

Implement a custom UIViewController for all of your applications to inherent from. In this view controller implement logic in the viewWillAppear message to determine and show the login screen if necessary.
//CustomViewController.h
#interface CustomViewController : UIViewController
#end
//CustomerViewController.m
#implementation CustomViewController
-(void)viewWillAppear:(BOOL)animated{
if(login_required){
LoginViewController *loginView = [[LoginViewController alloc] initWithNibName:#"LoginView" bundle:nil];
[self presentModalViewController:loginView animated:false];
}
}
#end
Then, simply, in your login view controller make sure you call:
[self dismissModalViewControllerAnimated:false];
The benefits of this approach are two fold. Firstly, it's a very simple implementation. However, most compellingly, having a base class for an application's view controller presents the opportunity to extract common logic.

Jason,
I have worked on a security tutorial provided by Chris Lowe on raywenderlich.com that was intended to demonstrate how to use basic iOS security to lock the application.
The premise behind this tutorial though was that the application would prompt for login upon first launch and if application was resumed upon unlocking the device through the use of NSNoftificationCenter in viewDidLoad and subscribe the the notifications: deviceWillLock and deviceWillUnlock. All of this assumes the device is set to lock.
Basic iOS Security Tutorial Part 2 - This is the part that has the NSNotification registration.
Basic iOS Security Tutorial Part 1 - This is the first part of the tutorial for clarity.

I also ran into this problem and came across this question whilst researching a solution. I didn't want to create the intermediate super class for my views and I wasn't sure how it would work out with navigation controllers. I have come up with another solution that works well for me - so thought I would share it. It is based around the use of NSNotificationCenter .
In your app delegate create a property to hold a reference to the currently displayed view controller - say currentViewController.
Then in the applicationDidFinishLaunching method, register a block observer to update the currentViewController property like this:
[[NSNotificationCenter defaultCenter] addObserverForName:#"CurrentViewChanged"
object:nil
queue:nil
usingBlock:^(NSNotification *note)
{self.currentViewController = (UIViewController *)note.object;} ];
In your view controller implementations, update the viewDidAppear methods to notify the observer that a new view controller is being displayed by adding the following line
[[NSNotificationCenter defaultCenter] postNotificationName:#"CurrentViewChanged" object:self];
Finally, include code in the applicationDidBecomeActive method in your app delegate to force the modal display of your login screen.
UIStoryboard *mainStoryBoard = self.window.rootViewController.storyboard;
UnlockViewController *uvc = [mainStoryBoard instantiateViewControllerWithIdentifier:#"modalUnlockView"];
uvc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self.currentViewController presentViewController:uvc animated:YES completion:NULL];
A couple of additional items to note :-
You can disable the login screen display at anytime by posting a notification where the view controller passed is nil.
You only need to post the notification once for a navigation view controller at the top level. All view controllers in the navigation controller stack will be covered. I haven't checked, but I suspect the same is true for a tab view controller.
If you want to display the login screen the first time you enter the app after startup then include the following line in the applicationDidFinishLaunching method.
self.currentViewController = self.window.rootViewController;
I hope this is of some use.
Thanks

Related

How to dismiss 3 modal view controllers at once?

I have an app that has an initial login screen then when the user wants to sign up, they are presented with a registration form that is three view controllers presented modally. When the user completes the form on the third screen (by pressing a "Done" button), I want the user to be taken back to the initial login screen.
I have tried doing this in the third view controller:
[self dismissViewControllerAnimated:NO completion:nil]
[self.presentingViewController dismissViewControllerAnimated:NO completion:nil]
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:NO completion:nil]
However it only dismissed two of the view controllers and not all 3. Why did this happen?
As other people pointed out, there are more elegant/efficient/easier ways to achieve similar results from the UX perspective: via a navigation controller, or a page view controller, or other container.
Short/quick answer: you need to go one step further in the chain of presenting view controllers, because the dismissal request needs to be sent to the controller that's presenting, and not to the one that's being presented. And you can send the dismiss request to that controller only, it will take care of popping from the stack the child controllers.
UIViewController *ctrl = self.presentingViewController.presentingViewController.presentingViewController;
[ctrl dismissViewControllerAnimated:NO completion:nil]
To explain why, and hopefully help other people better understand the controller presenting logic in iOS, below you can find are more details.
Let's start from Apple documentation on dismissViewControllerAnimated:completion:
Dismisses the view controller that was presented modally by the view controller.
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.
Thus [self dismissViewControllerAnimated:NO completion:nil] simply forwarded the request to self.presentingViewController. Which means the first two lines had the same effect (actually the 2nd line did nothing as there was no presented controller after the 1st one executed).
This is why your dismissal of view controllers worked only the top 2 ones. You should've start with self.presentingViewController and go along the chain of presenting view controllers. But this is not very elegant and can cause problems if later on the hierarchy of view controllers changes.
Continuing to read on the documentation, we stumble upon this:
If you present several view controllers in succession, thus building a stack of presented view controllers, calling this method on a view controller lower in the stack dismisses its immediate child view controller and all view controllers above that child on the stack.
So you needn't call dismissViewControllerAnimated:completion: three times, a call on the controller that you want to come back will suffice. At this point, passing a reference to that controller would be more reliable than navigating through the stack of view controllers.
There are some more useful details in the documentation, for example regarding what transitions apply when dismissing multiple controllers at once.
I recommend you go through the whole documentation, not only for this method, but for all methods/classes that you use in your application. You'll likely discover things that will make your life easier.
And if you don't have the time to read all Apple's documentation on UIKit, you can read it when you run into problems, like in this case with dismissViewControllerAnimated:completion: not working as you thought it would.
As a closing note, there are some more subtle issues with your approach, as the actual dismissal takes place in another runloop cycle, as it's possible to generate console warnings and not behave as expected. This is why further actions regarding presenting/dismissing other controllers should be done in the completion block, to give a change to UIKit to finish updating its internal state.
Totally understood. What I will do is embed a navigation controller instead of using modal. I have a case just like you. I have LoginViewController to be the root view controller of the UINavigationController. SignupViewController will be presented by push method. For ResetPasswordViewController, I will use modal because it's supposed to go back to LoginViewController no matter the results. Then, you can dismiss the whole UINavigationController from SignupViewController or LoginViewController.
Second approach will be like, you come up with your own mechanism to reference the presented UIViewController via a shared instance. Then, you can easily dismiss it. Be careful with the memory management. After dismissing it, you should consider whether you need to nil it right away.
I know three ways to dismiss several viewControllers:
Use a chain of completion blocks
~
UIViewController *theVC = self.presentingViewController;
UIViewController *theOtherVC = theVC.presentingViewController;
[self dismissViewControllerAnimated:NO
completion:^
{
[theVC dismissViewControllerAnimated:NO
completion:^
{
[theOtherVC dismissViewControllerAnimated:NO completion:nil];
}];
}];
Use 'viewWillAppear:' method of viewControllers
~
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.shouldDismiss)
{
CustomVC *theVC = (id)self.presentingViewController;
theVC.shouldDismiss = YES;
[self dismissViewControllerAnimated:NO completion:nil];
}
}
Pass a reference to LoginVC1 further down the chain.
(This is the best approach so far)
Imagine you have some StandardVC, which presented LoginVC1.
Then, LoginVC1 presented LoginVC2.
Then, LoginVC2 presented LoginVC3.
An easy way of doing what you want would be to call (from inside your LoginVC3.m file)
[myLoginVC1 dismissViewControllerAnimated:YES completion:nil];
In this case your LoginVC1 would lose its strong reference (from StandardVC), which means that both LoginVC2 and LoginVC3 would also be deallocated.
So, all you need to do is let your LoginVC3 know that LoginVC1 exists.
If you don't want to pass a reference of LoginVC1, you can use:
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:NO completion:nil];
However, the above approaches are NOT the correct ways of doing what you want to do.
I would recommend you doing the following:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if (!self.isUserLoggedIn)
{
[UIApplication sharedApplication].keyWindow.rootViewController = self.myLoginVC;
}
return YES;
}
Then, when user finished his login process, you can use
[UIApplication sharedApplication].keyWindow.rootViewController = self.myUsualStartVC;

How to call viewWillAppear with storyboard iOS7

I'm using storyboard.
As I remeber (I worked with ios 4, long time ago=)) everytime, when View appears, calls
-(void)viewWillAppear:(BOOL)Animated {}
method.
Now this method doesn't call, if I press Home button and run app again.
How to fix it?
I need to update one UIView if it appears after home pressing.
The function viewWillAppear is not part of UIView. It is part of UIViewController.
It is called after the view controller's view has been loaded and just before it starts to transition onto screen.
If you create a subclass of UIView and put this function in it then it will never be called because it isn't supposed to be.
Edit
You are correct that viewWillAppear does not get called when the app is coming back from the background.
If you want to update a part of your app when this happens then you can do something in the AppDelegate.
I'd recommend not trying to store properties etc... in the AppDelegate though. You should do something like this...
- (void)applicationWillEnterForeground:(UIApplication *)application
{
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
// just pass the message on. In your view you will need to add an observer for this notification
[[NSNotificationCenter defaultCenter] postNotificationName:#"UpdateViewNotification" object:nil];
}
try this, call the super,
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
}

iOS equivalent of Android finish()

I am relatively new to iOS development. I developed an app for Android first, and now am porting it to iOS. One thing that I can't figure out is how to make a ViewController "go away". I'm accustomed to the finish() method in Android. With that method, the current activity ends itself and the user is presented with the the previous screen that was open prior to opening the current screen.
What I'm trying to accomplish is making my "create" screen go away after a record is saved. In the Android world, I would just call the finish() method and that would be taken care of. What is the iOS equivalent?
I have tried the following code in my iOS app, hoping that the view would be animated away.
[self.navigationController popToRootViewControllerAnimated:YES];
Edit:
The view was presented as below.
SettingsViewController *vc = [[SettingsViewController alloc] initWithNibName:#"SettingsView"];
controllers = [NSArray arrayWithObject:vc];
self.sideMenu.navigationController.viewControllers = controllers;
[self.sideMenu setMenuState:MFSideMenuStateClosed];
where controllers is defined as such:
NSArray *controllers = nil;
How did you present this view controller?
Did you use presentViewController:animated:completion:? If so, you want something like this:
[self dismissViewControllerAnimated: YES];
If you are using pushViewController:animated:, you are not talking about a modal view. You are talking about a normal ViewController you pushed onto the stack. To "undo" this, you need to pop the view controller:
[self.navigationController popViewControllerAnimated: YES];
Though finish() and [self dismissViewControllerAnimated:YES] are similar in terms of functionality, but they are not exactly same, when we call finish() method in the Android, we are programatically telling the Android system to destroy the activity completely from the memory, when we override the onDestroy() activity life cycle callback and add a log, then logs are shown when finish() is called. onDestroy() is also called when the system needs resources and it frees the memory by finishing the activity. But
[self dismissViewControllerAnimated: YES];
does not remove the UIViewController instance from the memory, the equivalent callback method for onDestroy() in IOS is viewDidUnload() which is not called on the message dismissViewController. I think the IOS system can only free the memory for the UIViewController instance when system needs resources but we can't programatically do that.
after pushing your viewcontroller call this method
func finish(){
var navigationArray = self.navigationController?.viewControllers //To get all UIViewController stack as Array
navigationArray!.remove(at: (navigationArray?.count)! - 2) // To remove previous UIViewController
self.navigationController?.viewControllers = navigationArray!
}

View Controller Doesn't respond to Action Methods

I have a simple test app (in OSX) that has a view controller with a button in its view. The button's action method is in the view controller's class, and that IBAction is connected in IB (through File's Owner). When the button is clicked, I get an EXC_BAD_Access error (except occasionally I get -[NSRunLoop buttonClick:] instead). I've read a bunch of posts here on SO having to do with NSViewControllers not being in the responder chain, but also that specifically hooking the action method up in IB should work. The only code I have is this:
In the app delegate:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
TestController *controller = [[TestController alloc] initWithNibName:#"TestController" bundle:nil];
[self.window.contentView addSubview:controller.view];
}
And, in the TestController class, just this:
-(IBAction)buttonClick:(id)sender {
NSLog(#"%#",sender);
}
I have 2 questions. Why is this happening, and where is the correct mvc place to put IBActions for button methods (shouldn't controller classes handle these)?
First off, I believe in the App Delegate there is already a property called viewController already defined for you. You should use this code self.viewController = [[TestController alloc] initWithNibName:#"TestController" bundle:nil]; instead of TestController *controller = [[TestController alloc] initWithNibName:#"TestController" bundle:nil];. If that doesn't fix it, then I'm not sure what's wrong. Your code should return the attributes of the button you clicked. I created my own sample project and tested out your code (although my app delegate looked different than yours) and it worked fine.
Second, you should elaborate on your second question "Why is this happening, and where is the correct mvc place to put IBActions for button methods (shouldn't controller classes handle these)?". It's not very clear what you mean.
Hope this helps a little.
NSViewController will not respond to IBActions on mac os x on iPhone It is the correct place to put your IBAction as the view should only draw its content (data) not change it. But on Mac os x the NSViewController is for setting up the NSView it's no ment to respond to IBActions you have two choices one is to put your IBAction in your NSView or is create a NSWindowController .
On mac osx you have plenty of screen space and you will alway have views within a window you'll use NSViewController to add them to your window and to setup your view but the window is first in your responder chain then your NSWindowController . eg: you may have one window and two view controller showing the same view which may have 5 test fields in it and view controller one loads that with data but you can not edit the data then view controller two loads the same view with the same data but enable editing for the text fields so. but all action methods will go to the WindowController .
I recommend you to check your viewController's identity inspector.
It may indicate wrong custom class.
viewController created basically usually has name ViewController on the section.

pushViewController iphone not working

i am unable to get the pushViewController to work on a View Based Application on the iPhone. On my 'ProjectViewController' i have a IBAction with the following code :
-(IBAction)switchAugmented
{
ARViewController *viewController = [[ARViewController alloc] initWithDelegate:self];
[self.navigationController pushViewController:viewController animated:YES];
[viewController release];
}
When i run the program and press ibaction nothing happens. Besides that statement above do i need to do anything else to make the view appear? what am i missing?
(...) on a View Based Application (...)
You just have no UINavController! Try to embed your main view in UINavigationController and everything will start working.
Double check to make sure that the button you are pressing is connected to the right method in interface builder. Also try putting an NSLog statement in the switchAugmented to see if the method is getting called.
You also have to check and see if you have a UINavigationController instance, otherwise you won't be able to push a new view controller.
You won't be able to push a new view controller in a View Based Project. You need to create a Navigation Based Project or add an instance of the UINavigationController in your Main.nib (if your using a nib file) only then will the push view controller will work