Prevent access to UIViewControllers with tab bar controller (storyboard) - objective-c

I currently encountering a problem with my iOS application.
I am attempting to incorporate a gradual login pattern, i.e.: the use can access some of the app without being required to login.
Required features are as follows:
At all times the user can view all navigation items that require login
When the user attempts to access an uiview(controller) that requires login, they will be prompted with a UIAlertView asking them to log in. (Preferably the UIAlertView will appear when the app recognised the initiated segue destination is restricted).
At first I used a subclass of UIViewController that, in the designated initialiser (initWithCoder), would check NSUserDefaults to see if the user was logged in. I then subclassed off of that. Limitations were as follows:
Couldn't use other subclasses of UIViewController, namely UITableViewController
The UIAlertView came up after the view had appeared, which i am assuming would cause errors if the subclassed UIViewController assumed the user was logged in.
Question summary:
I would like to know how to conditionally prevent users from accessing certain UIView(Controller)s and subclasses of UIViewController, and when that happens present a UIAlertView.
Update 1
Could categories and/or protocols be a viable solution?
Update 2
CodaFi pointed out singletons as a great solution to manage the user's state.
With that implemented I now need to figure out how to control the user's access.
As I am using storyboards I feel that the most versatile implementation would be subclassing UIStoryboardSegue, and on the perform method check if the user is attempting to access an restricted UIViewController (perhaps restricted controllers have a protocol property that specifies the required status: logged in/out). However the pitfall here is that you cannot choose the class/subclass of a UIStoryboardSegue in the storyboard graphic editor. I am aware that I could do it programatically, however that seems tedious as i would have to add IBActions and like that to methods that perform segues, furthermore I don't think that would work with the way elements such as navigationController and tabbarControllers behave.
Does anybody have a viable solution to restricting the user's navigation?
Update 3
I've added an answer to this question, however I still deem it as unanswered because the answer I've written doesn't take into account segues between navigation bar controllers. However it may help some people.

So, I've answered how to do this using custom segues.
Now I understand my real problem was the mental disconnect with the tabbarcontrollerdelegate protocol.
As a solution to preventing access to tabs I've made a class to handle said tab bar controller
#import <Foundation/Foundation.h>
#interface TabBarDelegate : NSObject<UITabBarControllerDelegate,UIAlertViewDelegate>{
UITabBarController *cachedTabBarController;
}
#end
#import "TabBarDelegate.h"
#implementation TabBarDelegate
-(BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController{
NSLog(#"%s",__FUNCTION__);
NSLog(#"Pretending the user isnt logged in");
if(true){
NSString *message = [NSString stringWithFormat:#"You require an account to access %#",viewController.tabBarItem.title];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Account Required" message:message delegate:self cancelButtonTitle:#"Okay" otherButtonTitles: #"Login",#"Create Account",nil];
[alert show];
//Hold tabbarcontroller property for alert callback
cachedTabBarController = tabBarController;
return false;
}
return true;
}
-(void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex{
if(cachedTabBarController){
switch (buttonIndex) {
case 1:
//Login
[cachedTabBarController performSegueWithIdentifier:#"tabBarToLogin" sender:cachedTabBarController];
break;
case 2:
//Sign up
[cachedTabBarController performSegueWithIdentifier:#"tabBarToSignup" sender:cachedTabBarController];
break;
default:
break;
}
//Release tab bar controller from memory
cachedTabBarController = nil;
}
}
#end
Then i wired it up to my appDelegate in applicationDidFinishLaunching...
//Hook up tab bar delegate
mainTabController = (UITabBarController*)self.window.rootViewController;
mainTabBarDelegate = [[TabBarDelegate alloc] init];
mainTabController.delegate = mainTabBarDelegate;
And Voila

Okay, So I have part of a solution.
It only works in situations where you can choose a custom segue (and I've only written code for pushing, not modal).
Protocol "UIViewControllerAuthentication" for all controllers that require authentication, this protocol contains an method to retrieve the required authentication status.
enum authenticationStatus {
notLoggedIn = 0,
noPassword = 1,
loggedIn = 2
};
#protocol UIViewControllerAuthentication <NSObject>
-(enum authenticationStatus)authStatusRequired;
#end
Controllers that require authentication conform to this protocol:
-(enum authenticationStatus)authStatusRequired{
return loggedIn;
}
Then use this for the authenticated push segue
#interface SeguePushWithAuth : UIStoryboardSegue
#end
-(void)perform{
NSLog(#"custom segue destination : %#",self.destinationViewController);
NSLog(#"custom segue source : %#",self.sourceViewController);
NSLog(#"custom segue dest conforms to protocol : %i",[self.destinationViewController conformsToProtocol:#protocol(UIViewControllerAuthentication)]);
if([self.destinationViewController conformsToProtocol:#protocol(UIViewControllerAuthentication)]){
UIViewController <UIViewControllerAuthentication> *destination = (UIViewController <UIViewControllerAuthentication> *)self.destinationViewController;
if (!((int)[AccountModel userAuthStatus] >= (int)[destination authStatusRequired])) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Authentication" message:#"You need an account to access this area" delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Login",#"Create Account", nil];
//alert
[alert show];
return;
}
}
[[self.sourceViewController navigationController] pushViewController:self.destinationViewController animated:true];
}
#end
Then use custom segues in uistoryboard
This is a good pattern to allow the destination controller to cancel the segue.
However its far from what I want as I want to be able to authenticate segues that link tab bar controllers to their children.

Related

UIView Login screen to tabbar logic

Folks,
I'm having trouble with some navigation logic. Currently I have a simple two tabbed tabbar application. But I want to show a loginscreen in front. So that would be an UIView.
Currently the code is as follows:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
UIViewController *viewController1 = [[roosterViewController alloc] initWithNibName:#"roosterViewController" bundle:nil];
UIViewController *viewController2 = [[SecondViewController alloc] initWithNibName:#"SecondViewController" bundle:nil];
self.tabBarController = [[UITabBarController alloc] init];
self.tabBarController.viewControllers = #[viewController1, viewController2];
self.window.rootViewController = self.tabBarController;
[self.window makeKeyAndVisible];
return YES;
}
So this pushes a simple tabcontroller. Well, now I want to have a login screen. So that would be a simple UIView which pushes the tabbar controller. But I can't seem to see the logic on how to do this.
I've been trying to present a modal view controller, but the thing is: the tabbar will be loaded on the background. Since I need the username/password information to work on the tabbarview, this won't work.
My Logic would be:
delegate > load loginViewController > load tabbar controller
But, then I need to be able to "logout". So I need to destroy the tabbar controller and present the login screen.
Any thoughts on this?
You could do something like this:
You could create a protocol which your AppDelegate conforms to.
#protocol Authenticator <NSObject>
#required
- (void)authenticateWithUsername:(NSString *)username andPassword:(NSString *)password;
- (bool)authenticated;
#optional
- (void)authenticationSuccess;
- (void)authenticationFailure;
...
#interface AppDelegate : UIResponder <UIApplication, Authenticator>
#property (readonly, nonatomic, assign) bool loggedIn;
...
#implementation AppDelegate
#synthesize loggedIn = _loggedIn;
- (void)authenticateWithUsername:(NSString *)username andPassword:(NSString *)password
{
//if success
_loggedIn = YES;
//check if app responds to the optional authenticateSuccess method
//call it if it does
//else fail
//do stuff
}
- (bool)authenticated
{
if (_loggedIn != NULL) {
return _loggedIn;
}
//do other stuff
}
...
I am a bit fuzzy on proper objective-c conventions and syntax so forgive me if I have a few errors, but anyways that is some logic and pseudo-code to work off of. Tweak that to your needs.
I hope this helps.
EDIT:
I guess my answer was a bit unfinished. It seemed to to me the answer to your question was strongly connected to some kind of authentication structure. If your app is so closely connected to authentication, then why not control its flow through the authentication structure. I guess that was my point, and since all this would be conveniently and readily available in your apps delegate, you could call these methods anywhere you wanted, therefore letting your authentication logic decide which view controller to show for example.
As mentioned have the login as the rootView and if the login is successful enable the other tabbarButtons else don't enable them like this..initially set the bool to false on view load then if successful enable a tabbar button else don't.
login = TRUE;
UITabBarItem *reportit = [[[[self tabBarController]tabBar]items] objectAtIndex:2];
[reportit setEnabled:TRUE];
else {
UITabBarItem *reportit = [[[[self tabBarController]tabBar]items] objectAtIndex:2];
[reportit setEnabled:FALSE];
}
I recommend you set the login screen as root. When the login is successful, you simply change the root of the window to the tabbar controller, with a nice animation.
I would recommend that you set up the tab bar just like you have it right now, but immediately following the makeKeyAndVisible you instantiate and present the login view controller, without animation.
This way once the app has launched the user does not see the tabBarController, but only the modally presented login screen. Once login is done you simply dismiss the login view controller and beneath it appears .... drumroll the tabbarcontroller!
Expanding on Levi's answer, this is how you switch the root view controller continuously (with an animation). Just add this extension:
extension UIViewController
{
func transitionToRootViewController(viewController:UIViewController)
{
UIView.transitionWithView(self.view.window,
duration: 0.3,
options: UIViewAnimationOptions.TransitionCrossDissolve,
animations: {
window.rootViewController = viewController
},
completion: nil
)
}
}
...to the UIViewController class (that will make the method transitionToRootViewController () available to all your view controllers), and call it on the exiting view controller, passing the entering view controller (that you perhaps instantiated form a separate storybord, who knows...) as a parameter.
(The basic idea was taken from here. I simplified the code a bit)
Beware though: I tried this code to insert a tab bar controller, whose selected index (tab) contained a navigation controller. During the transition animation, the navigation controller's navigation bar "underlaps" the status bar (carrier, clock, battery level), and only after the transition animation completes, it "jumps" into its place immediately, creating a horribly distracting (and unpolished) effect.
I fixed it by adding the following code to the child view controller embedded in the navigation controller:
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
self.navigationController?.navigationBar.layer.removeAllAnimations()
// ^ THIS
}
(This fix was taken from this forum.)
I haven't checked, but the same issue might occur whenever you transition into a navigation controller (or any plain-vanilla UIViewController that happens to have a navigation bar attached), whether it is embedded in a tab bar controller or not.

Objective C - UIAlertViewDelegate cannot access UILabel in other class

I am new to Objective C and having an issue that I know must be a very simple one - I think perhaps I am approaching it the wrong way.
I have created an UIAlertViewDelegate class that I want to be the delegate for some UIAlertViews that I have in my View Controller class.
When the user presses a button and enters some text, I would like a label in my ViewController class to be updated with that text. However, I obviously cannot reference the label "outputLabel" from my UIAlertViewDelegate class unless I pass it in some way.
The error I get is "Use of undeclared identifier 'outputLabel'. "
ViewController.h
#interface ViewController : UIViewController <UIAlertViewDelegate>;
...
#property (strong) UIAlertViewDelegate *AVDelegate;
ViewController.m
- (void)viewDidLoad
{
UIAlertViewDelegate *AVDelegate;
AVDelegate = [[UIAlertViewDelegate alloc] init];
[super viewDidLoad];
}
- (IBAction)doAlertInput:(id)sender {
UIAlertViewDelegate *AVDelegate;
AVDelegate = [[UIAlertViewDelegate alloc] init];
UIAlertView *alertDialogue;
alertDialogue = [[UIAlertView alloc]
initWithTitle:#"Email Address"
message:#"Please Enter Your Email Address:"
delegate:self.AVDelegate
cancelButtonTitle:#"OK"
otherButtonTitles:nil, nil];
alertDialogue.alertViewStyle=UIAlertViewStylePlainTextInput;
[alertDialogue show];
}
UIAlertViewDelegate.m
#import "UIAlertViewDelegate.h"
#implementation UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView
clickedButtonAtIndex:(NSInteger)buttonIndex
{
NSString *buttonTitle = [alertView buttonTitleAtIndex:buttonIndex];
NSString *alertTitle = [alertView title];
if ([alertTitle isEqualToString:#"Alert Button Selected"] || [alertTitle isEqualToString:#"Alert Button Selected"]) {
if ([buttonTitle isEqualToString:#"Maybe Later"])
{
outputLabel.text=#"User Clicked: 'Maybe Later'";
UIAlertViewDelegate.h
#interface UIAlertViewDelegate : UIViewController <UIAlertViewDelegate,UIActionSheetDelegate>;
I've done some work many years ago with Java. I imagine I have to pass my UIAlertViewDelegate class the view somehow but Im not sure on the syntax, would greatly appreciate a pointer in the right direction...
James
There are a variety of things here that you shouldn't be doing but, starting with the main ones affecting your question....
You shouldn't be inventing your own UIAlertViewDelegate class, which means don't do this...
#interface UIAlertViewDelegate : UIViewController <UIAlertViewDelegate,UIActionSheetDelegate>;
Because UIAlertViewDelegate is a protocol, you also can't use alloc with it.
The main thing that's correct is...
#interface ViewController : UIViewController <UIAlertViewDelegate>
That says that your ViewController is able to act as a UIAlertViewDelegate and therefore your code for creating the alert can use delegate:self instead of trying to create a separate delegate property or object.
You are likely to still have questions but those changes should clean it up a bit.
Your view controller is the delegate for the alert view, therefore, you don’t need to crate the delegate object.
init the alert view using self (the view controller) as the delegate argument.
You then implement the delegate methods (for the alert view) within the view controller.
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIAlertViewDelegate_Protocol/UIAlertViewDelegate/UIAlertViewDelegate.html
The point of delegation is to make it possible for an object to have callbacks on any class.
I would suggest reading over protocols and delegation.
http://developer.apple.com/library/ios/#documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html#//apple_ref/doc/uid/TP40008195-CH14-SW1
I'm guessing you are trying to use the alert view to input text, so you would set the alert view style to UIAlertViewPlainTextStyle, and setting the label in one of the callbacks by accessing the text field from the alertView object.

Dismissing modal view controller from app delegate

I am using the facebook SDK to login to my app. If the user is not logged in, the login VC modally appears. Once the user has tapped log in, it alerts the App Delegate if the login was successful. If it was, I want to dismiss the modal login VC. How do I do this from the app delegate?
You could try dismissing the presented ViewController, as something has to present the modal view controller
UINavigationController *navigationController = (id) self.window.rootViewController;
[[navigationController presentedViewController] dismissModalViewControllerAnimated:NO];
If you would want to check if a specific ViewController is being presented (i.e. only dismiss when a certain one is shown) then you could add in a check.
UIViewController *viewController = [navigationController presentedViewController];
if ([viewController isMemberOfClass:[YourViewController class]]) {
[viewController dismissModalViewControllerAnimated:NO];
}
The appDelegate needs some way to know who the hosting viewController is, so it can send the dismiss message. You need to figure out some way to make this happen. One way would be to define an ivar on the appDelegate "callDismissOnMeIfFaceBookFails", and set it when you are in this situation, otherwise its nil.
Note if its nil, the appDelegate can send the dismiss message with no overhead no problems! Use nil messaging to your advantage (I use it all the time). [Aside: I see so much code "if(obj) [obj message];" Don't do the if - just send the message - if obj is nil it has no effect and is handled efficiently!]
EDIT:
So you have a class AppDelegate. In the #interface define a property:
#property (nonatomic, strong) UIViewController *callDismissOnMeIfFaceBookFails;
and in the implementation you #synthesize it. Define a method:
- (void)dismissLoginView
{
[callDismissOnMeIfFaceBookFails dismissModalViewControllerAnimated:YES];
callDismissOnMeIfFaceBookFails = nil; // don't need it now, this unretains it
}
So, before the modal view controller is presented, the presenting object sets the appDelegate property "callDismissOnMeIfFaceBookFails" to itself.
When the user has successfully logged in, the login object sends the message to appDelegate telling it to dismissLoginView.

NSObject extending UIAlertViewDelegate crash?

I have a helper class that I want to display a UIAlertView then will perform some web service action. I would like to do it this way so I can re-use the alert view in other places.
I am declaring it like so:
#interface FBLinkHelper : NSObject <UIAlertViewDelegate>
I also have a method that will show the alert view:
- (void) showLinkDialog {
UIAlertView *av = [[UIAlertView alloc] initWithTitle:#"Link account with Facebook?" message:#"Linking your account with Facebook extends your user experience. Link account?" delegate:self cancelButtonTitle:#"No" otherButtonTitles:#"Yes", nil];
[av show];
}
This is how I am showing the dialog:
FBLinkHelper *fbLinkHelper = [[FBLinkHelper alloc] init];
[fbLinkHelper showLinkDialog];
All seems pretty normal to me. It shows the alert view dialog, but when I click a button (either one) my app crashes without much to say in the console. I notice it only crashes when I have a UIAlertViewDelegate method in place, such as didDismissWithButtonIndex. Why is this happening?
-UPDATE-
The FBLinker instance was not a strong property of its caller, so the delegate callback went to a dangling pointer. OP reports making FBLinker strong took care of issue.
Original suggestion of making the class extend from UIView instead of NSObject had no effect, and is referenced here only for historical purposes.
I had the same case in an app I'm working at. If the delegate object isn't a strong (or retain in iOS prior to 5.0) property it might get deallocated right after the UIAlertView is shown.

show alert xcode and go back after alert

i want to show the alert and when somebody click on OK they need to be send to the page before. How can i make this?
I use the following code:
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"BOOYAH!"
message:#"Saved" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
Considering you have 1 option on the alert view and the delegate is self. Use this method in the same .m file as the code above
- (void)alertView:(UIAlertView *)alertV didDismissWithButtonIndex:(NSInteger)buttonIndex
{
//go back a page
[alertV autorelease];
}
Don't forget to release the alert view. I added it in the delegate method, but you can choose to release it right after showing it (only 1 release though)
Assign the UIAlertViewDelegate to self and then implement the following method is called
- (void) alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
if (buttonIndex == buttonIndexForOK) { // Where buttonIndexForOK is the index for your ok button, in this case, that would be zero, but if you want an OK and a Cancel button this would be different.
// go back to the last page
}
}
UIAlertView follows the delegation design pattern that is extremely common in iOS development. You provide a delegate object, and when the object wants to tell you about something, it sends that delegate object a message.
In your code, you've provided self as the delegate. This means that this object needs to conform to the UIAlertViewDelegate protocol.
You will see that there are several methods you can implement to react to various events relating to the alert view. You should use the alertView:clickedButtonAtIndex: method, which provides an index parameter indicating which button was tapped.