Newbie question about design patterns in objC.
I'm writing a functionality for my iphone app which I plan to use in other apps too. The functionality is written over two classes - Viewcontroller1 and Viewcontroller2. Viewcontroller1 is the root view of a navigation controller and it can push Viewcontroller2. Rest of the app will use only ViewController1 and will never access Viewcontroller2 directly. However, triggered
by user events, Viewcontroller2 has to send a message to the
rest of the app.
My question is what is the best way of achieving it?
Currently, I use two level of delegation to send the message out from Viewcontroller2. First send it to Viewcontroller1 and then let Viewcontroller1 send it to rest of the app or the application delegate. So my code looks like -
//Viewcontroller1.h
#protocol bellDelegate
-(int)bellRang:(int)size;
#end
#interface Viewcontroller1 : UITableViewController <dummydelegate> {
id <bellDelegate> delegate;
#end
//Viewcontroller1.m
#implementation Viewcontroller1
-(void)viewDidLoad {
//some stuff here
Viewcontroller2 *vc2 = [[Viewcontroller2 alloc] init];
vc2.delegate = self;
[self.navigationController pushViewController:vc2
animated:YES];
}
-(int)dummyBell:(int)size {
return([self.delegate bellRang:size]);
}
//Viewcontroller2.h
#protocol dummyDelegate
-(int)dummyBell:(int)size;
#end
#interface Viewcontroller2 : UITableViewController {
id <dummyDelegate> delegate;
#end
//Viewcontroller2.m
#implementation Viewcontroller2
-(int)eventFoo:(int)size {
rval = [self.delegate dummyBell:size];
}
#end
That's the proper way of doing things ! If all your delegate method are to be invoked on viewController2, you could have only one protocol and directly assigned the delegate from viewController1 to viewControler2 but it will block you the first time tou need to invoke the delegate from viewControler1.. bad design then !
I would say the implementation is fine, but this isn't necessarily a case where you should add another level of abstraction to get reusability, because it's just a general and recommended way to pass messages around objects, i.e. "delegation" Apple's documentation on delegation.
Also, regarding your problem case where you have to send a message "to the rest of the app", you should consider notifications, they may be very time-saving in some situations. (Apple's documentation on notifications, SO Question about Delegation vs Notifications)
Related
I'm working on an app and I need to pass data between view controllers. I know this is a common question but I couldn't find an answer for my problem : I'm able to pass data from the FirstViewController (MasterViewController in my case) to the SecondViewController (SettingsViewController) but not the reverse. What happens is that I call a method from the FirstViewController in my SecondViewController.m file. This works and it logs the data. But when I quit the SecondViewController (using [[self presentingViewController] dismissViewControllerAnimated:YES completion:nil];) the data is reset.
I tried using other methods to pass data but it didn't work. I'm using this code to pass data:
MasterViewController *vc = [[MasterViewController alloc] initWithNibName:#"MasterViewController" bundle:nil];
[vc setPorts:SelectedPorts];
I also tried replacing [vc setPorts:SelectedPorts]; with vc.selectedCellIndexes = SelectedPorts; but the same problem occurs.
the setPorts method is declared in the FirstViewController.h file and SelectedPorts is a variable I declared in SecondViewController.m (it's not nil I checked).
Here's the setPorts: in FirstViewController.m :
- (void) setPorts:(id)selectedPorts {
selectedCellIndexes = selectedPorts;
NSLog(#"selectedCellIndexes : %#", selectedCellIndexes);
}
This logs the good value but when I log it in viewWillAppear in FirstViewController.m it's reset to the value it has before I called the method from SecondViewController.m.
Just to clarify, if I DON'T quit the SecondViewController.m, the data isn't reset.
I did read all your comments, and I really thanks you for your help. for convenience, I used a global variable.
Thanks for your help.
You have a list of ports in MasterViewController and you expect to use it in the SettingsViewController.
The MasterViewController can hold this list and SettingsViewController should have an access to it.
In SettingsViewController, have a setSelectedPort method:
#property (nonatomic, retain) id selectedPorts
- (void) setPorts:(id)selectedPorts;
The method saves the selected ports list into a property.
In MasterViewController, call the SettingsViewController and give it the list.
SettingsViewController *vc = [[SettingsViewController alloc] initWithNibName:#"SettingsViewController" bundle:nil];
[vc setSelectedPorts:yourValue];
When the list is modified inside the SettingsViewController, the list of ports contained in MasterViewController won't move even if you leave the SettingsViewController.
In secondViewController, You create protocol
#import <UIKit/UIKit.h>
#protocol sampleDelegate <NSObject>
- (void)passValue:(id)selectedPorts
#end
#interface SecondViewController : UIViewController
#property (nonatomic, strong) id <sampleDelegate> passDelegate;
#end
In viewDidLoad or wherever method as per your need, call method like this,
[self.passDelegate passValue:selectedPorts];
In FirstViewController.h,
Import the delegate <sampleDelegate>,
#import "SecondViewController.h"
#interface FirstViewController : UIViewController<SampleDelegate>
#end
In FirstViewController.m,
-(void)passValue:(id)selectedPorts
{
id receivedValues = selectedPorts;
}
and set self in your SecondViewController allocation,
SecondViewController *vc = [[SecondViewController alloc] initWithNibName:#"SecondViewController" bundle:nil];
vc.passDelegate = self;
There is nothing unusual in the getting result. By doing
MasterViewController *vc = [[MasterViewController alloc] initWithNibName:#"MasterViewController" bundle:nil];
[vc setPorts:SelectedPorts];
You are creating a new instance of MasterViewController from your SecondViewController. This is not the same from which you navigated to the SecondViewController. So you wont get the expected result. Since you are setting the ports([vc setPorts:SelectedPorts]) to the newly created instance of the Master.
Instead of creating a new instance,just hold the reference of the MasterViewController in SecondViewController in a property and assign it before moving to second VC. As a beginner I suggested this way. But using delegate is the prefferred way passing data back.
Either use delegate methods to communicate with the master VC from the modal VC, or you could do something like this if you want to retrieive some manipulated objects from the modal VC.
Set the object(s) as properties in the modal view controller's .h-file (so they are public).
Using unwind segues, in the master VC, just do this:
-(IBAction)exitModalVC:(UIStoryboardSegue*)segue
{
SomeObject *obj = ((YourModalVC*)segue.sourceViewController).someObject;
//Do what you want with obj
}
EDIT:
This will only work if you are using unwind segue (which is a neat way of dismissing modal VC when using story board)
And you are using this, which is not unwind segues:
[[self presentingViewController] dismissViewControllerAnimated:YES completion:nil];
You were creating a new instance of the first view controller from the 2nd view controller not accessing the same instance of the original caller. That was the reason why while you could see the logs but data were not there when you got back to the original caller - your MasterViewController.
You need to use delegate method. Check my answer for this SO.
This is problem related to object ownership.
Follow the below steps:
As per understanding you want reverse value from "SecondViewController" to "FirstViewController"
Don't create new object of FirstViewController in SecondViewController, it will not work.
Create object of "FirstViewController" in "SecondViewController.h" file.
#property (nonatomic,strong) FirstViewController *firstViewController;
When you navigate from FirstViewController to SecondViewController, please pass the "self".
e.g. SecondViewController *vc = [[SecondViewController alloc] initWithNibName:#"SecondViewController" bundle:nil];
vc.firstViewController = self;
If you want pass the reverse value to FirstViewController then in SecondViewController.m file.
[self.firstViewController setPorts:SelectedPorts];
And in FirstViewController.m refresh your controls with latest values.
Try above code will defiantly work as per your requirement.
I am aware that this topic has been covered already in many places, but while going through the different answers, I could not find a solution that fits my case.
Basically what I am trying to do is very simple. I have a view controller with a table view, and a + button that fires a second view controller where the user can enter, say a name, and this name is then added to the first view controller table view. Think about Contacts on the iPhone where you can add a new person (or Amazon where you can add a new credit card).
My question is - what is the best way to return this string (in this case) back to the view controller where the table is?
People suggested using NSDefaults, Delegate, or singleton none of which are really good for this case (each one for its own reason). I really just need to return a simple string.
Thank you for your help.
If you are navigating from View Controller A ---> View Controller B, which is your case here, and then you want to pass information from B -> A, it is recommended to use a loose coupling, like delegation. There are a couple of things as you discussed, like NSUserDefaults, singleton, NSNotification, and may be many more.
But delegation is the better and standard approach to do it.
Here's what you need to do (please note I am typing this off the top of my head so the compiler may have some issues with my syntax).
In your child view controller interface:
#protocol ChildDelegate <NSObject>
- (void)didConfirmName:(NSString*)name
#end
#interface ChildViewController : UIViewController
...
#property (nonatomic, assign) id<ChildDelegate> delegate;
And implementation, inside the method called when the user confirms whatever they need to confirm:
- (void)myCustoMethod
{
...
if ([self.delegate respondsToSelector:#selector(didConfirmName:)])
[self.delegate didConfirmName:NAME_STRING];
}
And in your parent view controller, when you are instantiating the child view controller, assign SELF as the delegate:
ChildViewController *vc = [[ChildViewController alloc] init...];
vc.delegate = self;
And implement:
- (void)didConfirmName:(NSString*)name
{
// Do whatever you want with the name here...
}
Also make sure you tell the compiler that you are implementing the protocol :
#interface ParentViewController () <ChildDelegate>
#end
You don't return the new thing to the TableView. What you need is to update the source of information that feeds your tableView.
So where the data live is the place you need to go and add it, when you will come back to the UITableViewController you may need to tell the UITableView to reload it's data.
There is a handle on each UIViewController to it's parent if you absolutely need to communicate with it.
UIViewController *parentVC = aViewController.parentViewController;
Assuming that the ViewController that needs to get updated is the root view controller of the app, you can do the following:
YourViewController * yvc = [(YourAppDelegate *)[[UIApplication sharedApplication] delegate] viewController];
[yvc updateString:#"Updated String"];
Remember to:
#import "YourAppDelegate.h"
But Honestly I would use the delegate pattern or a NSNotification.
Three good options:
1) Have the first view controller pass itself to the second view controller, so that the second controller can send messages to the first. Ideally, create a protocol that the first controller adopts, so that the second controller doesn't depend on the type of the first controller but only cares that it implements the protocol:
#protocol Nameable
#property(copy) NSString* name;
#end;
#interface FirstViewController <Nameable>
//...
#end
#implementation FirstViewController
#synthesize name;
//...
#end
Then the first controller can do this:
SecondViewController *second = [[SecondViewController alloc]
initWithNibName:nil]; second.thingToName = self;
[self.navigationController pushViewController:second animated:YES];
And when the time comes, the second controller can do this:
self.thingToName.name = nameString;
The details aren't really that important -- the main thing to know here is that if you want to send a message to an object, you first need a pointer to the object. When the first view controller sets second.thingToName = self, it's providing that pointer.
2) Have the first view controller create a data object in which the second view controller can store the data, like this:
#interface Person <Nameable>
//...
#end
#implementation Person
#synthesize name;
//...
#end
Now the first view controller can create a new Person and pass that:
SecondViewController *second = [[SecondViewController alloc]
Person *person = [[Person alloc] init];
[self.people addObject:person];
initWithNibName:nil]; second.thingToName = person;
[person release];
[self.navigationController pushViewController:second animated:YES];
This is similar to the first approach, only the thing that's receiving the name isn't the view controller here, it's some sort of data container (Person).
You can see the value of the protocol here, too -- notice that the SecondViewController class doesn't change at all between the first and second approaches. It doesn't care whether it's talking to a view controller or an instance of Person or anything else... as long as the thing implements the Nameable protocol, it's happy.
3) Reverse the direction of communication. Instead of making the second view controller send the string, have its parent get the string. This may well be the simplest solution, although it does require that the parent has some way to know that the child is done. It'd go something like this:
#implementation FirstViewController
//...
- (IBAction)addNewName:(id)sender
{
self.secondController = [[SecondViewController alloc] initWithNibName:nil bundle:nil];
[self.navigationController pushViewController:self.secondController animated:YES];
}
- (void)viewWillAppear
{
if (self.secondController != nil) { // we must be returning from the child
self.name = self.secondController.name;
self.secondController = nil;
}
}
#end
I am relatively new to iPhone development so please excuse what I hope is an easy task/question.
I have an app with a tab bar with several views each controlled by a MVC group. On loading the app the first mvc group of the tabbar gets displayed. In the viewDidAppear method of this first view controller I have a login screen modally displayed like so:
- (void ) viewWillAppear:(BOOL)animated
{
LoginViewController *loginvc = [[LoginViewController alloc] init];
loginvc.delegate = self;
if (*xxxxx*) {
[loginvc setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
[self presentModalViewController:loginvc animated:NO];
}
[loginvc release];
}
For the conditional if statement I would like a method to be called which checks that the user is logged in. I have provisionally (just for testing purposes) placed this method in the loginvc view controller however this cannot be a long term solution as the loginvc gets dealloc-ed once its dismissed (its the modal view).
At the moment I have a class set up like the following but don't know if this is correct, and don't know where to instantiate it (should this be in my main app delegate method application:didFinishLaunchingWithOptions: ?), and don't know how to change the userIsLoggedIn variable:
LoginClass.h file
#import <Foundation/Foundation.h>
#interface LoginClass : NSObject {
BOOL userIsLoggedIn;
}
-(BOOL)checkIfUserIsLoggedIn;
#end
LoginClass.m file
#import "LoginClass.h"
#implementation LoginClass
- (id)init
{
self = [super init];
if (self) {
// Initialization code here.
userIsLoggedIn = NO;
}
return self;
}
-(BOOL)checkIfUserIsLoggedIn
{
return userIsLoggedIn;
}
#end
My question is how should I create an object of this class which exists for the whole time the application is alive. This is so that when the app is launched, the login screen is displayed, but after successful login the 'userIsLoggedIn' variable gets set to 'YES' so that if the first mvc is called again the login screen doesn't get displayed modally again.
I hope I have made sense and hope someone can help with the code. If I am approaching this wrong please let me know alternative strategies (for example should I even be calling the modal display from the viewDidAppear of the first mvc?).
Thanks in advance,
Andy
The App Delegate is the place where your Login class should be. It's always reachable by your view controllers and "lives" for the whole time your app is running. Check the docs to see how to access your App Delegate from your view controllers.
Your Login Class should also look like this:
#import <Foundation/Foundation.h>
#interface LoginClass : NSObject {
BOOL userIsLoggedIn;
}
#property (nonatomic) BOOL userIsLoggedIn;
-(BOOL)checkIfUserIsLoggedIn;
#end
// Implemenatation
#import "LoginClass.h"
#implementation LoginClass
#synthesize userIsLoggedIn;
- (id)init
{
self = [super init];
if (self) {
// Initialization code here.
userIsLoggedIn = NO;
}
return self;
}
-(BOOL)checkIfUserIsLoggedIn
{
return userIsLoggedIn;
}
#end
To set the login state, simply do something like this:
[[yourAppDelegateVar instanceOfLoginClass] setUserIsLoggedIn:YES];
or
[[yourAppDelegateVar instanceOfLoginClass] setUserIsLoggedIn:NO];
You can use the Singleton pattern, which is totally suitable for your problem.
Sure, you may use the AppDelegate for that as suggested below (as UIApplication is itself a singleton and therefore the AppDelegate -- the object that is the delegate of your UIApplication singleton -- only exists in one instance too) but is it better practice to keep the AppDelegate only for what it is designed for (i.e. the delegate of the UIApplication, namely managing everything that is related to app events like going to the background or back to the foreground, launching, receiving push notifs, and so on) to keep code clear and readable.
You better use another dedicated singleton for each Service you need to implement: this is better design practice.
For example implementing a LoginService singleton class (See here in the Apple doc for implementation details) that will hold the -(BOOL)loginWithUsername:(NSString*)username password:(NSString*)password; method (that checks that your login/pwd is OK, potentially communicating with your WebService for this), a #property(readonly) BOOL loggedIn; property, probably a #property(readonly) NSString* currentUsername; if needed, and so on.
I have two view controllers, firstViewController and secondViewController. I am using this code to switch to my secondViewController (I am also passing a string to it):
secondViewController *second = [[secondViewController alloc] initWithNibName:nil bundle:nil];
second.myString = #"This text is passed from firstViewController!";
second.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentModalViewController:second animated:YES];
[second release];
I then use this code in secondViewController to switch back to the firstViewController:
[self dismissModalViewControllerAnimated:YES];
All of this works fine. My question is, how would I pass data to the firstViewController? I would like to pass a different string into the firstViewController from the secondViewController.
You need to use delegate protocols... Here's how to do it:
Declare a protocol in your secondViewController's header file. It should look like this:
#import <UIKit/UIKit.h>
#protocol SecondDelegate <NSObject>
-(void)secondViewControllerDismissed:(NSString *)stringForFirst
#end
#interface SecondViewController : UIViewController
{
id myDelegate;
}
#property (nonatomic, assign) id<SecondDelegate> myDelegate;
Don't forget to synthesize the myDelegate in your implementation (SecondViewController.m) file:
#synthesize myDelegate;
In your FirstViewController's header file subscribe to the SecondDelegate protocol by doing this:
#import "SecondViewController.h"
#interface FirstViewController:UIViewController <SecondDelegate>
Now when you instantiate SecondViewController in FirstViewController you should do the following:
// If you're using a view controller built with Interface Builder.
SecondViewController *second = [[SecondViewController alloc] initWithNibName:"SecondViewController" bundle:[NSBundle mainBundle]];
// If you're using a view controller built programmatically.
SecondViewController *second = [SecondViewController new]; // Convenience initializer that uses alloc] init]
second.myString = #"This text is passed from firstViewController!";
second.myDelegate = self;
second.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentModalViewController:second animated:YES];
[second release];
Lastly, in the implementation file for your first view controller (FirstViewController.m) implement the SecondDelegate's method for secondViewControllerDismissed:
- (void)secondViewControllerDismissed:(NSString *)stringForFirst
{
NSString *thisIsTheDesiredString = stringForFirst; //And there you have it.....
}
Now when you're about to dismiss the second view controller you want to invoke the method implemented in the first view controller. This part is simple. All you do is, in your second view controller, add some code before the dismiss code:
if([self.myDelegate respondsToSelector:#selector(secondViewControllerDismissed:)])
{
[self.myDelegate secondViewControllerDismissed:#"THIS IS THE STRING TO SEND!!!"];
}
[self dismissModalViewControllerAnimated:YES];
Delegate protocols are EXTREMELY, EXTREMELY, EXTREMELY useful. It would do you good to familiarize yourself with them :)
NSNotifications are another way to do this, but as a best practice, I prefer using it when I want to communicate across multiple viewControllers or objects. Here's an answer I posted earlier if you're curious about using NSNotifications: Firing events accross multiple viewcontrollers from a thread in the appdelegate
EDIT:
If you want to pass multiple arguments, the code before dismiss looks like this:
if([self.myDelegate respondsToSelector:#selector(secondViewControllerDismissed:argument2:argument3:)])
{
[self.myDelegate secondViewControllerDismissed:#"THIS IS THE STRING TO SEND!!!" argument2:someObject argument3:anotherObject];
}
[self dismissModalViewControllerAnimated:YES];
This means that your SecondDelegate method implementation inside your firstViewController will now look like:
- (void) secondViewControllerDismissed:(NSString*)stringForFirst argument2:(NSObject*)inObject1 argument3:(NSObject*)inObject2
{
NSString thisIsTheDesiredString = stringForFirst;
NSObject desiredObject1 = inObject1;
//....and so on
}
I could be way out of place here, but I am starting to much prefer the block syntax to the very verbose delegate/protocol approach. If you make vc2 from vc1, have a property on vc2 that you can set from vc1 that is a block!
#property (nonatomic, copy) void (^somethingHappenedInVC2)(NSString *response);
Then, when something happens in vc2 that you want to tell vc1 about, just execute the block that you defined in vc1!
self.somethingHappenedInVC2(#"Hello!");
This allows you to send data from vc2 back to vc1. Just like magic. IMO, this is a lot easier/cleaner than protocols. Blocks are awesome and need to be embraced as much as possible.
EDIT - Improved example
Let's say we have a mainVC that we want to present a modalVC on top of temporarily to get some input from a user. In order to present that modalVC from mainVC, we need to alloc/init it inside of mainVC. Pretty basic stuff. Well when we make this modalVC object, we can also set a block property on it that allows us to easily communicate between both vc objects. So let's take the example from above and put the follwing property in the .h file of modalVC:
#property (nonatomic, copy) void (^somethingHappenedInModalVC)(NSString *response);
Then, in our mainVC, after we have alloc/init'd a new modalVC object, you set the block property of modalVC like this:
ModalVC *modalVC = [[ModalVC alloc] init];
modalVC.somethingHappenedInModalVC = ^(NSString *response) {
NSLog(#"Something was selected in the modalVC, and this is what it was:%#", response);
}
So we are just setting the block property, and defining what happens when that block is executed.
Finally, in our modalVC, we could have a tableViewController that is backed by a dataSource array of strings. Once a row selection is made, we could do something like this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *selectedString = self.dataSource[indexPath.row];
self.somethingHappenedInModalVC(selectedString);
}
And of course, each time we select a row in modalVC, we are going to get a console output from our NSLog line back in mainVC. Hope that helps!
hmm, look for the notification centre and pass back info in a notification. here is apples take on it
- I take this approach personally unless any one has any other suggestions
Define a delegate protocol in the second view controller and make the first one the delegate of the second.
here is my navigation on my app
1) homescreenview controller-->composemessageviewcontroller (i am able to use delegate to send data back to homescreenview)
2)homescreenview controller -->messageslistcontroller(tableview)-->detailmessageviewcontroller(which is where my reply button is).
my problem is when i hit reply i want to send back information to homescreenviewcontroller with delegate . how can i do this?
Thanks in advance.
----UPDATE
#XJones, thanks for the detailed explanaion. is this what is should be doing in when i push detailview? please correct me if i am wrong.
(void)pushDetailMessageController{
DetailMessagetController *detailmessage = [[DetailMessagetController alloc] init];
detailmessage.delegate = self;
// push messageListController onto navigation controller here
[detail release];
}
that's a very general question. you're basically asking how to pass information from one controller to another controller. There are different ways to do that, a protocol (what a delegate generally communicates through) is one of them. The quickest thing you can do w/o making assumptions in your code that may be problematic later would be to pass the homeScreenController along as you push messageListController and then detailMessageController. You'll need to define an iVar and property in messageListController and detailMessageController to do this.
Something like:
in messageListController.h:
#import "HomeScreenController.h"
#interface messageListController : UITableViewController {
// your iVars
HomeScreenController *homeScreenController;
#end
#property (nonatomic, assign) HomeScreenController * homeScreenController;
add the same iVar and property for homeScreenController to detailMessageController.
in homeScreenController.h:
- (void)pushMessageListController
{
MessageListController *messageListController = [[MessageListController alloc] init];
messageListController.homeScreenController = self;
// push messageListController onto navigation controller here
[messageListController release];
}
in messageListController do the same thing as above when creating and pushing detailMessageController. Now, in detailMessageController you can send messages directly to homeScreenController.
If you want to generalize the above implementation so your controllers aren't specifically knowledgeable about each other then you can define a protocol and pass homeScreenController through as a delegate supporting that protocol.
How about adding a method in the messageslistcontroller? I personally would add the delegate "homescreenview" to the detailmessageviewcontroller as the messageslistcontroller doesn't have anything to do with the reply and apperantly the homescreenviewcontroller does.
when you create the detailmessageviewcontroller in the messageslistcontroller do this:
detailmessageviewcontroller.homeScreenDelegate = self.delegate;
One approach (without delegation)
as you are using navigationController, so
[[self.navigationController viewControllers]objectAtIndex:0] will always return you homeScreenViewController.....you can use this object....
Thanks,