Protocol and Delegates, passing data from a modal view form to all the tab bar controllers - objective-c

I'm implementing a tab bar app in which every tab there's a controller. When the app runs, I throw up a modal view with a login form. Once the user logs in, I dismiss this modal view and I would like to pass the username to all the tab bar controllers.
I created a Protocol in the modal view controller LoginViewController.h:
#protocol PassUserInfoDelegate <NSObject>
#required
- (void) passUserInfo: (NSString *)string;
#end
#interface LoginViewController : UIViewController <UIApplicationDelegate> {
id <PassUserInfoDelegate> delegate;
}
and somewhere in the implementation LoginViewController.h I call [[self delegate] passUserInfo:idJson]; in order to pass the idJson value to the delegates.
Then, in the HomeViewController.h I do:
#interface HomeViewController : UIViewController <PassUserInfoDelegate>
so this controller is a delegate of the Protocol.
and in the implementation HomeViewController.m I create the Protocol's method:
- (void) passUserInfo:(NSString *)string
{
NSLog(#"I got the string = %#", string);
}
and I assign the HomeViewController as the delegate of the Protocol in LoginViewController:
- (void)viewDidLoad
{
loginViewController = [[LoginViewController alloc] init];
[loginViewController setDelegate:self];
[super viewDidLoad];
}
So far, this is working. I get the idJson string from the Protocol to this class.
What I want to do now, is to do the same thing in another class, and I want that class to get the idJson value too. So I created in my Home2ViewController.h the same thing:
#interface HomeView2Controller : UIViewController <PassUserInfoDelegate>
and created the Protocol method:
- (void) passUserInfo:(NSString *)string
{
NSLog(#"I got the string = %#", string);
}
- (void)viewDidLoad
{
loginViewController = [[LoginViewController alloc] init];
[loginViewController setDelegate:self];
[super viewDidLoad];
}
but this the passUserInfo method in this class is never called.
What am I doing wrong?
Thanks in advance!!

One of your problems is that your HomeViewController2's view is only loaded the first time you access it so your viewDidLoad method is only going to be called the first time you tap the tab bar item that corresponds to HomeViewController2.
There are a couple of other issues with your code that are somewhat unrelated to your questions but to give you a hint, move the [super viewDidLoad] call to the TOP of your -(void)viewDidLoad method.
Also note that if your viewDidLoad method on HomeViewController2 was being called, you would effectively be creating a new instance of your modal (login) view which would not contain the username info entered in the previous instance. So even if the method was being called, there would be no user info to pass to your controller.
The bottom line is that you don't need to pass the username to all view controllers. Create a User class object (subclass of NSObject) with instance variables for all user attributes you want to keep (i.e. username, email, firstname, lastname etc) and make it a singleton instance (this is optional but will make sure you only have one instance of this class throughout your app lifecycle).
Instantiate the class in your app delegate or wherever you first need to access the User objet (probably in your login view controller), set the attributes you need to set and dismiss the modal view controller.
From here on, you can simply request for the User instance and access its attributes from anywhere in your code, including the different view controller in your tab bar controller.
Here's a simple example of an initialisation method for a singleton User object:
#implementation User
static User *user_ = nil;
+ (User*)sharedInstance
{
if (user_ == nil)
{
user_ = [[super allocWithZone:NULL] init];
}
return user_;
}
And here's how you can access the user object after importing the "User.h" header file anywhere in your code:
User *user = [User sharedInstance];

Related

Cocoa Subclassing weirdness

I'm trying to understand how Subclassing works in Cocoa.
I've created a new Cocoa Application Project in XCode 5.1.
I drag a new Custom View onto the main window.
I create a new Objective-C class CustomViewClass and set it as a Subclass of NSView. This generates the following :
CustomViewClass.h
#import <Cocoa/Cocoa.h>
#interface CustomViewClass : NSView
#end
CustomViewClass.m
#import "CustomViewClass.h"
#implementation CustomViewClass
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
NSLog(#"Custom View initialised");
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
// Drawing code here.
}
#end
Note that I added the NSLog(#"Custom View initialised"); line so I can track what is going on.
In interface Builder, I select the Custom View and within the Idenditiy Inspecter set it's custom Class to CustomView. Then I run the Application.
As expected I get a Custom View initialised message in the Console.
I do exactly the same with an NSTextField adding it to the window, creating a new class TextFieldClass and the NSTextField custom Class is to TextFieldClass. I also add a NSLog(#"Text Field initialised"); in the same place as above to track things.
However when I run the App, I only get the Custom View initialised message in the Console and not the NSLog(#"Text Field initialised");message.
So initially I think that NSTextField doesn't recieve the initWithFrame message when it is created. So I add an initialiser to TextFieldClass :
- (id)init {
self = [super init];
if (self) {
// Initialization code here.
NSLog(#"Text Field initialised");
}
return self;
}
However this still doesn't seem to get called.
I assumed therefore that NSTextField just wasn't being subclassed. However, when I add this method to TextFieldClass :
-(void)textDidChange:(NSNotification *)notification {
NSLog(#"My text changed");
}
Run the app and lo and behold, every time I type in the text field I get the My text changed message in the Console.
So my question is, what is going on here? How does the NSTextField get initialized and how can you override it's initialiser?
Why does the Custom View seem to act differently to the NSTextField?
Source code here
For your first question, NSTextFiled gets initialised via
- (id)initWithCoder:(NSCoder *)aDecoder
In this case, you have dragged a NSTextField from the palette and then changed the class to your custom text field class in the identity inspector. Hence the initWithCoder: will be called instead of initWithFrame:. The same is true for any object (other than Custom View) dragged from the palette
Instead, if you drag "Custom View" from the palette and change the class to your custom text field class, the initWithFrame: will be invoked.
The CustomViewClass you have created is the second case, hence initWithFrame: is invoked. The TextFieldClass is the first case, hence initWithCoder: is invoked.
If you use the Interface Builder in XCode, you should use awakeFromNib to initialise your subclass.
- (void)awakeFromNib
{
// Your init code here.
}
If you want to use your subclass programatically and using the interface builder, then use code like this:
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self initView];
}
return self;
}
- (void)awakeFromNib
{
[self initView];
}
- (void)initView
{
// Your init code here
}

Passing data back and forth using AppDelegate

To start I am building an app to learn the basics of Objective-C. If there is anything unclear please let me know and I will edit my question.
The app is supposed to have the next functionality.
Open the camera preview when the app is executed. On the top there is a button to go to a TemplateController where the user can select an array of frames to select from a UICollectionView. User selects the Template and returns to the Camera Preview. User takes a picture and the picture with the frame selected is shown in the PreviewController. If the user doesn't like the frame and wants to switch it for another one. PreviewController has button on top to go to the TemplateController, select the frame and go back again to the PreviewController with the new frame.
I do not want to create an object for the frame everytime. I want the AppDelegate to hold that object. To keep it alive per say?(sorry, English is not my mother tongue).
I was thinking to use NSUserDefaults BUT I really want to do it using the AppDelegate. So at this point NSUserDefaults is not an option.
Now, I am using storyboards with a navigation controller. A screenshot is available here
Right now when I pass from the TemplateController to my PreviewController my code looks like this:
Reaching TemplateController from MainController or PreviewController
- (IBAction)showFrameSelector:(id)sender
{
UIStoryboard *storyboard;
storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil];
TemplateController *templateController = [storyboard instantiateViewControllerWithIdentifier:#"TemplateController"];
templateController.frameDelegate = self;
[self presentViewController:templateController animated:YES completion:nil];
}
Passing the data from TemplateController to its controller's destiny (Either MainController or PreviewController)
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
_selectedLabelStr = [self.frameImages[indexPath.section] objectAtIndex:indexPath.row];
[self.collectionView deselectItemAtIndexPath:indexPath animated:NO];
[self dismissViewControllerAnimated:YES completion:^{
if ([self.frameDelegate respondsToSelector:#selector(templateControllerLoadFrame:)])
{
[self.frameDelegate performSelector:#selector(templateControllerLoadFrame:) withObject:self];
}
}];
}
This loads the selected frame in PreviewController
- (void)templateControllerLoadFrame:(TemplateController *)sender
{
UIImage *tmp = [UIImage imageNamed:sender.selectedLabelStr];
_frameImageView.image = tmp;
}
My problem is, I don't have very clear what changes I have to do on the AppDelegate(it is untouched right now). What would be the best approach to accomplish this?
Main issue is when Tamplate is chosen before taking the still image. If I select the frame after taking the picture then it displays.
I am not certain that I understand your question. Stuffing an object into the app delegate solution may not be the best way forward. In fact I believe you ought to look at the delegation pattern that is used by Apple to communicate between view controllers. Please note that you appear to be doing half of the delegate pattern already. For example you make your PreviewController a frameDelegate of the TemplateController.
So I would think you'd have something like the following to transfer information from TemplateController back to the PreviewController. Note that I've included prepare for segue as that is a common pattern to push a data object forward (it will be called if you connect a segue from the PreviewController to the TemplateController and in your action method call performSegueWithIdentifier:#"SegueTitle"). Use of the "templateControllerDidFinish" delegation method is a common pattern used to push information back from TemplateController when it closes.
TemplateController.h
#class TemplateController;
#protocol TemplateControllerDelegate <NSObject>
-(void) templateControllerDidFinish :(TemplateController*)controller;
#end
#interface TemplateController : UIViewController
#property (nonatomic, weak) id <TemplateControllerDelegate>delegate;
...
#end
TemplateController.m
//! The internals for this method can also be called from wherever in your code you need to dismiss the TemplateController by copying the internal
-(IBAction)doneButtonAction:(id)sender
{
__weak TemplateController*weakSelf = self;
[self dismissViewControllerAnimated:YES completion:^{
[self.delegate templateControllerDidFinish:weakSelf];
}];
}
PreviewController.h
#import "TemplateController.h"
#interface PreviewController<TemplateControllerDelegate>
...
#end
PreviewController.m
#implementation
...
-(void) templateControllerDidFinish :(TemplateController*)controller
{
self.dataProperty = controller.someImportantData;
...
}
...
-(void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
if ( [[segue identifier]isEqualToString:#""] )
{
TemplateController *tc = [segue destinationViewController];
tc.delegate = self;
tc.data = [someDataObjectFromPreviewController];
}
}
To fix this situation a bit more:
Add a segue from the PreviewController to the TemplateController
(Ctrl-drag from Preview view controller to the Template Controller
in the document outline mode)
Name the segue identifier in the identity inspector
Change your code that presents the view controller from:
(IBAction)showFrameSelector:(id)sender
{
UIStoryboard *storyboard;
storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil];
TemplateController *templateController = [storyboard instantiateViewControllerWithIdentifier:#"TemplateController"];
templateController.frameDelegate = self;
[self presentViewController:templateController animated:YES completion:nil];
}
to
- (IBAction)showFrameSelector:(id)sender
{
[self performSegueWithIdentifier:#"SegueTitle"];
}
Add your data object to the target view controller as noted in prepareForSegue and you will be in good shape. Then use the delegate method to catch any data returned from your template (just add the data as properties to the controller and you should be golden)
You can see a better example of this delegation in a utility project template from Xcode (I just keyed this in..) I hope this information helps. You can get more information at these resources and also by searching Google and SO for iOS delegation :
Concepts in Objective C (Delegates and Data Sources)
Cocoa Core Competencies

UITableView isn't displaying data

AppDelegate
- (void)applicationDidFinishLaunching:(UIApplication *)application {
UIStoryboard *myStoryboard = [UIStoryboard storyboardWithName:#"MainSB" bundle:nil];
tCont = [myStoryboard instantiateViewControllerWithIdentifier:#"Table"];
}
- (void)serviceAdded:(NSNetService *)service moreComing:(BOOL)more {
tCont.server = _server;
[tCont addService:service moreComing:more];
}
TableController
- (void)addService:(NSNetService *)service moreComing:(BOOL)more {
[self.services addObject:service];
if(!more) {
[self.tableView reloadData];
}
}
server is correctly carried over, and service is added to services. But I don't see it in my table view. Is it because I'm trying to instantiate the view before it exists?
Update: I've done a little more debugging and found that TableController is initialized before my delegate calls for it. I've only seen one TableController thread, so my code is most likely the issue.
You need to create an UIWindow, set its rootViewController property to your table view controller and send it the makeKeyAndVisible message. You also have to implement the tableView:cellForRowAtIndexPath: method on your table view controller.

Does a view reset all its variables every time it opens?

I have a int that resets itself every time the view re-opens/leaves. I have tried every way of declaring the int that i can think of, from public, to instance variable to global variable, but it still seems to reset!
#interface MainGameDisplay : UIViewController
extern int theDay;
#implementation MainGameDisplay
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"%i", theDay);
}
- (IBAction)returnToHome:(id)sender {
ViewController *new = [[ViewController alloc] initWithNibName:nil bundle:nil];
[self presentViewController: new animated:YES completion:NULL];
NSLog(#"%i", theDay);
}
- (IBAction)theDayAdder:(id)sender {
theDay++;
}
Okay so theDay is a global integer variable. on View load NSLog returns an output of 0. I can then click theDayAdder as many times as I want, and when I click returnToHome, it will tell me what theDay is. When I come back to MainGameDisplay page however, theDay will be reset back to zero, even though it is a global variable?
Output:
0
N (number of times you clicked 'theDayAdder' button)
0
The problem is that you alloc init'ing a new instance of MainGameDisplay every time you go back to it, so of course your global variable will be reset to 0. You need to create a property (typed strong) in ViewController, use that to go back to the same instance each time.
- (IBAction)returnToGameDisplay:(id)sender {
if (! self.mgd) {
self.mgd = [[MainGameDisplay alloc] initWithNibName:nil bundle:nil];
}
[self presentViewController: self.mgd animated:YES completion:NULL];
NSLog(#"%i", theDay);
}
In this example mgd is the property name created in the .h file.
You should know that viewDidLoad() is called when the view is loaded--not when when the view "opens" as you say. You might have a view opened in a retained value and re-opened time and time again and have vieDidLoad() called only once. However, whenever the view becomes visible, then viewWillAppear() is the delegate that is called. So, try outputting your value in viewWillAppear()--instead of viewDidLoad() and call the view appropriately (i.e., have it stick around and not created every time you need it). This will keep the view from being destroyed between calls. The code for your view should look like the following:
#interface MainGameDisplay : UIViewController
extern int theDay;
#implementation MainGameDisplay
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void) viewWillAppear:(BOOL) animated {
[super viewWillAppear:animated];
NSLog(#"%i", theDay);
}
- (IBAction)returnToHome:(id)sender {
ViewController *new = [[ViewController alloc] initWithNibName:nil bundle:nil];
[self presentViewController: new animated:YES completion:NULL];
NSLog(#"%i", theDay);
}
- (IBAction)theDayAdder:(id)sender {
theDay++;
}
The parent of the view (I assume the appDelegate) should do the following
#property (nonatomic, strong) MainGameDisplay *mainGameDisplay = [[MainGameDisplay alloc] initWithNib:#"MainGameDisplay" …]
ViewDidLoad() is called once--after the view is created and loaded. However, viewWillAppear() and other functions triggered by IBAction etc. are called appropriately.
extern variables are meant to be constant. If you expect your MainGameDisplay class to be long-lived, or if theDay is otherwise only supposed to be tied to that class, why not either declare theDay as a property, or, if you only ever need to set it internally in MainGameDisplay, as an ivar.
The other alternative, if you want that value to continue to exist independently of the class instance where it's declared, is to declare it static. A static var will retain its value, even across the lifetime of different instances of the class where it's declared.

It is possible to use an existing ViewController with PerformSegueWithIdentifier?

I use the method performSegueWithIdentifier:sender: to open a new ViewController from a storyboard-file programmatically. This works like a charm.
But on every time when this method is being called, a new ViewController would be created. Is it possible to use the existing ViewController, if it exista? I don't find anything about this issue (apple-doc, Stack Overflow, ...).
The Problem is:
On the created ViewController the user set some form-Elements and if the ViewController would be called again, the form-elements has the initial settings :(
Any help would be appreciated.
Edit:
I appreciate the many responses. Meanwhile, I'm not familiar with the project and can not check your answers.
Use shouldPerforSegueWithIdentifier to either allow the segue to perform or to cancel the segue and manually add your ViewController. Retain a pointer in the prepareForSegue.
... header
#property (strong, nonatomic) MyViewController *myVC;
... implementation
-(BOOL) shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender{
if([identifier isEqualToString:#"MySegueIdentifier"]){
if(self.myVC){
// push on the viewController
[self.navigationController pushViewController:self.myVC animated:YES];
// cancel segue
return NO;
}
}
// allow the segue to perform
return YES;
}
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString:#"MySegueIdentifier"]){
// this will only be called the first time the segue fires for this identifier
// retian a pointer to the view controller
self.myVC = segue.destinationViewController;
}
}
To reuse an existing UIViewController instance with a segue create the segue from scratch and provide your own (existing) destination (UIViewController). Do not forget to call prepareForSegue: if needed.
For example:
UIStoryboardSegue* aSegue = [[UIStoryboardSegue alloc] initWithIdentifier:#"yourSegueIdentifier" source:self destination:self.existingViewController]
[self prepareForSegue:aSegue sender:self];
[aSegue perform];
Following code makes singleton view controller.
Add them to your destination view controller implementation, then segue will reuse the same vc.
static id s_singleton = nil;
+ (id) alloc {
if(s_singleton != nil)
return s_singleton;
return [super alloc];
}
- (id) initWithCoder:(NSCoder *)aDecoder {
if(s_singleton != nil)
return s_singleton;
self = [super initWithCoder:aDecoder];
if(self) {
s_singleton = self;
}
return self;
}
I faced this problem today and what I have done is to create the view controller manually and store it's reference.
Then every time I need the controller, check first if exists.
Something like this:
MyController *controller = [storedControllers valueForKey:#"controllerName"];
if (!controller)
{
controller = [[UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:NULL] instantiateViewControllerWithIdentifier:#"MyControllerIdentifierOnTheStoryboard"];
[storedControllers setValue:controller forKey:#"controllerName"];
}
[self.navigationController pushViewController:controller animated:YES];
Hope it helps.
Create a property for the controller.
#property (nonatomic, weak) MyController controller;
And use some kind of lazy initialization in performSegueWithIdentifier:sender
if (self.controller == nil)
{
self.controller = [MyController alloc] init]
...
}
In this case, if controller was already created, it will be reused.
Firstly you would be going against Apple's design in Using Segues: "A segue always presents a new view controller".
To understand why it might help to know that what a segue does is create a new view controller and then the perform calls either showViewController or showDetailViewController depending on what kind of segue it is. So if you have an existing view controller just call those methods! e.g.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
Event *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
self.detailViewController.detailItem = object;
[self showDetailViewController:self.detailViewController.navigationController sender:self];
}
You would need to make the Viewcontroller into a singleton class.