Hi im developing an app that has a parent view that then used containers to embed other views as seen below.
For now im only working with the left and centre container which are both table views. The main view or the Project screen view is my parent controller and i want it to pass data to and from the two child controller and i know for this the best option is to use delegates. However each example i have looked at that uses delegates, created and initialises a new view controller so for example lets say the left container embeds a view using the leftviewcontroller. Each example has this line of code.
LeftViewController *sampleProtocol = [[LeftViewController alloc]init];
LeftViewController.delegate = self;
Im thinking i dont need to create a new LeftViewController since it is embeded it is already in my list of child controllers. So my queston is how would i get the controller from the list of child controllers and set the parent as the delegate. I know i it is an array and i can use objectAtIndex but how do i know the order of items in the array will not change can i not call it but a tag or identifier? Thank you for any help sorry if the question is not that clear its my first time setting up delegates.
i know for this the best option is to use delegates.
In this case, I wouldn't be so sure. I think the best option would be to have a robust model and use KVO and notifications to signal updates between view controllers.
The direct answer to your question is not too bad.
for (UIViewController *viewController in self.childViewControllers) {
if ([viewController isKindOfClass:[LeftViewController class]]) {
LeftViewController *leftViewController = (id)viewController;
leftViewController.delegate = self;
break;
}
}
I think a minor improvement on this would be to use the segue. Make sure each of the containers have a named segue. In this example, the left view controller has a segue with the identifier "Load Child LeftViewController".
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"Load Child LeftViewController"]) {
LeftViewController *leftViewController = segue.destinationViewController;
leftViewController.delefate = self;
}
}
Its always better to use NSNotificationCenter for such complex mechanism.
*** put following code in LeftController.m ***
// *** Register a Notification to recieve a Data when something happens in Center controller ***
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(receivedNotification:)
name:#"hasSomeData"
object:nil];
// *** create a method to receive Notification data ***
- (void)receivedNotification:(NSNotification *) notification {
if ([[notification name] isEqualToString:#"hasSomeData"])
{
// do your stuff here with data
NSLog(#"data %#",[notification object]);
}
}
*** when something happen in center controller post a notification to inform Left Controller ***
[[NSNotificationCenter defaultCenter] postNotificationName:#"hasSomeData" object:self];
//Secondvc.h
#protocol Sendmessage<NSObject>
#required
-(void)Object:(NSArray *)tosend;
#end
#interface Secondvc:UIViewcontroller{
id <Sendmessage> delegate;
}
#property(strong,nonatomic) id <Sendmessage> delegate;
#end
//Secondvc.m
#implementation Secondvc
#synthesize delegate;
-(void)viewDidLoad{
//Do Something here!
}
//Pass Some Value When a button event occured in Second vc
-(IBAction)Send_Data{
[self dismissViewControllerAnimated:Yes completion:nil];
[self.delegate Object:[NSArray Arraywithobjects:#"Hello",nil]];
}
#end
//FirstVc.h
#import "Secondvc.h"
#interface FirstVc<Sendmessage>
#end
//FirstVc.m
#implementation FirstVc
-(void)viewDidLoad{
Secondvc* Object=[[Secondvc alloc]init];
Object.delegate=self;
}
#pragma mark Secondvc Deklegate method implementation
-(void)Object:(NSArray *)tosend{
NSLog(#"Recieved data Form Second VC Is:\n%#",tosend);
}
#end
HTH!Enjoy Coding.
Related
I have MainController which is the (UIViewController) main view in the app, and MenuController which is a UITableView.
In the MainController.h
- (void) menu1:(NSInteger ) row;
In the MainController.m
- (void) menu1:(NSInteger ) row{
switch(row){
case 0:
//DO SOMETHING.......
break;
default:
break;
}
}
I want "menu1" to make action when I click a cell in the MenuController.
I made this: (in MenuController.m)
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.section == 0){
[menu1:indexPath.row]; //<=== What should I do to make it work ?
}
}
You can access the method using your UINavigationController.
So in your MainController.h, add this:
- (void) menu1:(NSInteger)row;
Now in your MenuController.m:
#import "MainController.h";
and change didSelectRowAtIndexPathto:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.section == 0){
MainController *mC = [self.navigationController.viewControllers objectAtIndex:0]; // Change the objectAtIndex number to the number of your MainController in the navigationController view hierarchy
[mC menu1:indexPath.row];
}
}
Your menu1 method is an instance method on the MainController, so you'll want to send the message to an instance of that class. Like so:
MainController *mainVC = [[MainController alloc] init];
[mainVC menu1:indexPath.row];
Or, if you were to define your menu1 method as a class method, you could just send the message to the MainController without initializing. Use plusses instead of minuses when defining your menu1 method, then call like this:
[MainController menu1:indexPath.row];
However, this probably isn't the best way for you to pass data back and forth between View Controllers. You'll likely want to at the very least set a property on your MenuController, and then send the message to that (of course, setting the property wherever it makes sense).
[self.mainController menu1:indexPath.row];
But another really good method for calling a method on another View Controller is the delegate pattern, but I'll leave that up to you to research on your own.
https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html
in your MenuController.h
You get back to your previous view like this.
[[self navigationController] popToViewController:yourController animated:YES];
or
[self.navigationController popToRootViewControllerAnimated:animated];
and use UINotificationCenter or Delegate pattern.
If you have no idea about them UINoitifcation is easier but Delegate pattern is better in this situation. I ll explain how to use UINotificationCenter.
Throw notification from your menuController like this.
[[NSNotificationCenter defaultCenter]
postNotificationName:#"yourNotificationName"
object:nil ];
This code is for catch notification write it down to your viewappear method.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(yourMethodAboutWhatYouWantToDoAfterCatchNotification:)
name:#"yourNotificationName"
object:nil ];
Do what you want to do after catch it
- (void) yourMethodAboutWhatYouWantToDoAfterCatchNotification:(NSNotification *) notification
{
// do your job.
}
And in viewDidDisappear remove it. Or you can catch it multiple times after time.
[[NSNotificationCenter defaultCenter] removeObserver:self name:#"yourNOtificationName" object:nil];
There are two ways to achieve this.
Implement a protocol, whose implementation should be there in MainController.m
In that implementation, you should call the method which you have in MainController.m.
Set the table views delegate to MainController instead of setting it as self (MenuController)
This will help to implement the delegate methods directly in the MainController.m, from which the menu1 method can be called.
Provided code below for the first way:
Create a protocol file, which will help to make a callback, and create a method.
//
// utilProtocol.h
// tempProject
//
#protocol utilProtocol <NSObject>
#optional
-(void)captureCellSelectForRow:(int)rowNumber;
#end
Now Import this file inside in MenuController.
#import "utilProtocol.h" // Inside MenuController
And create a variable for the protocol.
#property(readwrite,assign)id<utilProtocol>utilDelegate;
In MainController.h, Import the utilProtocol.h and have its delegate into it.
Don't forget to add utilProtocol
#import <UIKit/UIKit.h>
#import "utilProtocol.h"
#interface ViewController : UIViewController <utilProtocol>
#end
Now Implement the method in MainController.m
-(void)captureCellSelectForRow:(int)rowNumber
{
[menu1:rowNumber];
}
Also set the delegate for it when the tableVIew's object is created in MainController.m
[tableViewObject setUtilDelegate:self];
Now when ever didSelectRowAtIndexPath is called in MenuController, just call the method using protocol.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.section == 0){
[utilDelegate captureCellSelectedForRow:indexPath.row];
}
}
Using this, the method inside MainController can be called.
Here is my .h file
#import <UIKit/UIKit.h>
#interface PersonViewController : UIViewController
#property(strong,nonatomic) NSString *personTitle;
And here is my .m file
#interface PersonViewController ()
#property (weak, nonatomic) IBOutlet UILabel *titleView;
#end
#implementation PersonViewController
//stuff …
-(void)setPersonTitle:(NSString *)personTitle
{
[self.titleView setText:personTitle];// also self.titleView.text=personTitle
[self.titleView setNeedsDisplay];
NSLog(#"The title shoud match as %# :: %#",personTitle,self.titleView.text);
}
-(NSString *)personTitle
{
return self.titleView.text;
}
//… more stuff
#end
The logging shows that the value is (null) for self.titleView.text whereas personTitle prints the appropriate value.
I remember doing this same thing a number of times and it worked. Any ideas why it’s failing this time?
update I use storyboard to set my scenes. And I am using xcode-5 and iOS-7
update: how I call
The user clicks a button, leading to a push segue
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
NSLog(#"enter prepare for segue.");
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
if ([segue.identifier isEqualToString:the_identifier_for_person]) {
NSLog(#"segue to person is progressing“);
if ([segue.destinationViewController isKindOfClass:[PersonViewController class]]) {
NSLog(#"segue to person destination is a match");
PersonViewController *aPerson = (PersonViewController *)segue.destinationViewController;
aPerson.personTitle=((MyItem*)self.allItems[indexPath.row]).title;
NSLog(#"segue to person is done");
}
}
}
This sounds like you forgot to wire up your UILabel in the storyboard. Can you confirm that self.titleView is not null?
View controllers create their views on demand, but can spot that only via a call to view. When the view is loaded, your outlets will be populated.
Either call view to force loading or keep the string in abeyance until you get viewDidLoad.
(aside: prior to iOS 6, views would also be released in low-memory situations so the idiomatic thing is to store the string and populate on viewDidLoad)
Having accepted another answer, I wanted to show the pattern that I actually used to solve the problem, in case someone else comes looking. This pattern is best practice (yes, I forgot it for a long moment there).
#pragma mark - update UI
-(void)setPersonTitle:(NSString *)personTitle
{
_personTitle=personTitle;
if (self.view.window) [self updateUI];//only if I am on screen; or defer to viewWillAppear
}
-(void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self updateUI];
}
-(void)updateUI
{
self.titleView.text=self.personTitle;
}
It is always important to update the ui when the data has changed, which is why I must make the call inside setPersonTitle. But because the IBOutlets are not yet set when I set personTitle in prepareForSegue, then I must also make the call inside viewWillAppear.
Do you actually call the -(void)setPersonTitle:(NSString *)personTitle method?
It seems that you aren't calling it correctly which would result in the title being null.
After reviewing the prepareForSeque it is clear that you are not calling the method. You are actually just changing the #property named personTitle.
In the viewDidLoad you should have it so that self.titleView.text = self.personTitle;
I've spent a few hours on this trying to work it out myself but I give up!
I have a master-detail arrangement where the user input screen needs to call a function on another class to post to a web service. Upon completion of the asynchronous call, the class will then call a specified function. In this case, I'm just testing and all I want to do is go back to the main screen after the user input is accepted by the web service.
When the uses taps a button on the input screen (SetLocationViewController), the asynchronous operation is called in the class APIPostClass. After it is complete, I want SetLocationViewController to segue back to MasterViewController.
In APIPostClass.m in (called after the asynchronous op finishes)
-(void)callWhenDone {
NSLog(#"callWhenDone loaded.");
SetLocationViewController *SLVClassInstance = [[SetLocationViewController alloc] init];
[SLVClassInstance doSegue];
}
In SetLocationViewController.m
-(void) doSegue {
NSLog(#"doSegue loaded");
[self performSegueWithIdentifier:#"SetLocationViewControllerManualUnwind" sender:self];
}
Calling doSegue from an action on SetLocationViewController.m does work so I know my segue is ok but the above doesn't work. I get the error reason: 'Receiver () has no segue with identifier 'SetLocationViewControllerManualUnwind''
I'm guessing the reason is because of the alloc init way of initialising of the VC, but I don't know any better. Thus, how can I call a function on another class as if it was being called by it's own class?
Create a delegate it would be much more reliable and fast than Notifications.
#protocol APIPostDelegate <NSObject>
#required
-(void)OnRequestSucess;
#end
In your APIPost add new property for delegate
#interface APIPost : NSObject
#property (weak) id<APIPostDelegate> delegate;
In SetLocationViewController implement APIPostDelegate
SetLocationViewController.h
SetLocationViewController :NSObject<APIPostDelegate>
SetLocationViewController.m
-(void)OnRequestSucess
{
[self doSegue];
}
before you make call to method on APIPost, assign self to delegate property.
APIPost *apipost=[[APIPost alloc]init];
apipost.delegate=self;
[apipost <your api method>];
APIPost.m
[self.delegate OnRequestSucess];
Hope this helps.
There are a few methods to make it happens:-
Use Delegate
Use NSNotification.
The way described by Artur above (For SplitViewController Only - iPad)
You should use delegate whenever it is possible but it might not be too straight forward. NSNotification is more straight forward but it is not a good practice and not a good programming style.
I will only share the NSNotification method as it is easier to implement.
In SetLocationViewController.m
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(doSegue) name:#"calldoSegue" object:nil];
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter]removeObserver:self name:#"calldoSegue" object:nil];
}
-(void) doSegue {
NSLog(#"doSegue loaded");
[self performSegueWithIdentifier:#"SetLocationViewControllerManualUnwind" sender:self];
}
In APIPostClass.m
-(void)callWhenDone {
NSLog(#"callWhenDone loaded.");
[[NSNotificationCenter defaultCenter]postNotificationName:#"calldoSegue" object:nil];
}
The above code should work but again, this is not a good practice. You should try to learn the Delegate method.
The answer is here: Performing segue from another class
In my APIPostClass.h, I setup the view controller:
#interface APIPostClass : NSObject {
SetLocationViewController *setLocationViewController;
}
#property(nonatomic, strong) SetLocationViewController *setLocationViewController;
#end
In my APIPostClass.m, I synthesize it:
#synthesize setLocationViewController;
then, instead of this (as in my question):
-(void)callWhenDone {
NSLog(#"callWhenDone loaded.");
SetLocationViewController *SLVClassInstance = [[SetLocationViewController alloc] init];
[SLVClassInstance doSegue];
}
I have:
-(void)callWhenDone {
NSLog(#"callWhenDone loaded");
[self.setLocationViewController doSegue];
}
Over in SetLocationViewController.m, the segue method remains unchanged:
-(void) doSegue {
NSLog(#"doSegue loaded");
[self performSegueWithIdentifier:#"SetLocationViewControllerManualUnwind" sender:self];
}
But when I call my API, I need to "attach" (forgive my terminology) the view controller to it. This is what I had:
- (IBAction)btnTestAPICall:(id)sender {
NSLog(#"User tapped API button");
APIPostClass *APIPostClassInstance = [[APIPostClass alloc] init];
[APIPostClassInstance APICall: ... ....
}
But this is what works after bringing all of the above:
- (IBAction)btnTestAPICall:(id)sender {
NSLog(#"User tapped API button");
APIPostClass *APIPostClassInstance= [[APIPostClass alloc] init];
UIViewController *currentVC=self;
APIPostClassInstance.setLocationViewController = currentVC;
[APIPostClassInstance APICall: ... ...
I hope this will help someone else!
Okay I am kind of new to IOS development, but I am writing an application where I am using a timer class to time out the user if they idle too long on any particular scene in my storyboard and it bumps the user back to the original scene/view. I have a single story board that is made up of several scenes/views(not sure what the correct word here is), and each scene has its own view controller.
I accomplish the timeout via the appdelegate class. See code below.
So I have the code working and it works great, but I am trying to make it so that it will ignore the timer if we are on the main scene.
I have googled this, read copious amounts of documentation, and have tried many things but so far I haven't been able to figure out how to get the currently viewed scene in the applicationDidTimeout method.
If I can get the name of the currently viewed scene/view, then I can choose to ignore the timer or not.
Does anyone know how to do this?
Thank you for your time.
#import "StoryboardAppDelegate.h"
#import "TIMERUIApplication.h"
#implementation StoryboardAppDelegate
#synthesize window = _window;
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// applicaiton has timed out
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(applicationDidTimeout:) name:kApplicationDidTimeoutNotification object:nil];
return YES;
}
-(void)applicationDidTimeout:(NSNotification *) notif
{
NSLog (#"time exceeded!!");
UIViewController *controller = [[UIStoryboard storyboardWithName:#"Main" bundle:NULL] instantiateViewControllerWithIdentifier:#"StoryboardViewController"];
UINavigationController * navigation = [[UINavigationController alloc]initWithRootViewController:controller];
[self.window setRootViewController:navigation];
navigation.delegate = self;
navigation.navigationBarHidden = YES;
if (controller) {
#try {
[navigation pushViewController:controller animated:NO];
} #catch (NSException * ex) {
//“Pushing the same view controller instance more than once is not supported”
//NSInvalidArgumentException
NSLog(#"Exception: [%#]:%#",[ex class], ex );
NSLog(#"ex.name:'%#'", ex.name);
NSLog(#"ex.reason:'%#'", ex.reason);
//Full error includes class pointer address so only care if it starts with this error
NSRange range = [ex.reason rangeOfString:#"Pushing the same view controller instance more than once is not supported"];
if ([ex.name isEqualToString:#"NSInvalidArgumentException"] &&
range.location != NSNotFound) {
//view controller already exists in the stack - just pop back to it
[navigation popToViewController:controller animated:NO];
} else {
NSLog(#"ERROR:UNHANDLED EXCEPTION TYPE:%#", ex);
}
} #finally {
//NSLog(#"finally");
}
} else {
NSLog(#"ERROR:pushViewController: viewController is nil");
}
[(TIMERUIApplication *)[UIApplication sharedApplication] resetIdleTimer];
}
#end
I'm assuming you've written the logic for the timer somewhere. Can you just invalidate the timer when you've popped back to the rootViewController?
Also instead of pushing a viewController onto the navigationViewController and handling the errors, you should check to see if the controller you're pushing is already in the stack like so:
if (![navigation.viewControllers containsObject:viewController] {
// push onto the stack
}
You could also check to see how many levels are currently in the navigationController by checking the count of the viewControllers array like so:
if ([navigation.viewControllers count] == 0) {
// I know we're on the main screen because no additional viewControllers have been added to the stack.
}
If you are not using modal controllers anywhere then the simplest solution would be
UINavigationController* nav = (UINavigationController*)self.window.rootViewController; // You could just save the nav as part of your app delegate
if (nav.viewControllers.count > 1){
[nav popToRootViewControllerAnimated:YES];
}
This is different then your current code because your main page will not be deleted and recreated every time the timer goes off
Okay I figured out how to do this. I was making this way too complicated.
To solve this I simply made a property and method in the app delegate class where I could set a scene name.
Then in each view controller header file I import the header file for the app delegate class and define a reference to it. Then in the load event for each view I simply set the scene name in the app delegate class using this line of code.
[myAppDelegate setSceneName:self.title];
Easy peasy!
I'm trying to use storyboard and get things working properly. I've added a a Container View to one of my existing views. When I try to add a reference to this in my view controller .h file (ctrl-drag), I get a IBOutlet UIView *containerView. How do I get a reference to the container view's view controller instead? I need the container view controller so I can set it's delegate to my view's controller so they can "talk" to each other.
I have my story board setup as:
And its referenced in my .h file as:
Notice in the .h that is is a UIView, not my InstallViewController for the view. How do I add a reference to the view controller? I need to be able to set its delegate.
There is another solution by specifying an identifier for the embed segue(s) and retrieve the corresponding view controllers in method prepareForSegue:
The advantage of this way is that you needn't rely on a specific order in which your child view controllers are added due to the fact that each child view controller is embedded via an unique segue identifier.
Update 2013-01-17 - Example
- (void) prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
// -- Master View Controller
if ([segue.identifier isEqualToString:c_SegueIdEmbedMasterVC])
{
self.masterViewController = segue.destinationViewController;
// ...
}
// -- Detail View Controller
else if ([segue.identifier isEqualToString:c_SegueIdEmbedDetailVC])
{
self.detailViewController = segue.destinationViewController;
// ...
}
}
c_SegueIdEmbedMasterVC & c_SegueIdEmbedDetailVC are constants with the corresponding ID of the segue IDs defined in the storyboard.
When you add a container view the xcode calls the UIViewController method addChildViewController:
In your case, you can get the container ViewController looking for it on the SplashViewController's list of childViewControllers, something like this:
for (UIViewController *childViewController in [self childViewControllers])
{
if ([childViewController isKindOfClass:[InstallViewController class]])
{
//found container view controller
InstallViewController *installViewController = (InstallViewController *)childViewController;
//do something with your container view viewcontroller
break;
}
}
I had the same doubt yesterday :)
The answer of Vitor Franchi is correct but could be more performant and convenient. Especially when accessing the child view controller several times.
Create a readonly property
#interface MyViewController ()
#property (nonatomic, weak, readonly) InstallViewController *cachedInstallViewController;
#end
Then create a convenient getter method
- (InstallViewController *)installViewController
{
if (_cachedInstallViewController) return _cachedInstallViewController;
__block InstallViewController *blockInstallViewController = nil;
NSArray *childViewControllers = self.childViewControllers;
[childViewControllers enumerateObjectsUsingBlock:^(id childViewController, NSUInteger idx, BOOL *stop) {
if ([childViewController isMemberOfClass:InstallViewController.class])
{
blockInstallViewController = childViewController;
*stop = YES;
}
}];
_cachedInstallViewController = blockInstallViewController;
return _cachedInstallViewController;
}
From now on access the child view controller that way
[self.installViewController doSomething];
UIView* viewInsideOfContainer = installerView.subviews[0];
Will give you the UIView inside of the UIViewController that your controller UIView references. You can cast the subview to any type that inherits from UIView.
If the nib is loaded it will call addChildViewController as part of the initialisation process
so a performant solution could be also to overwrite
- (void)addChildViewController:(UIViewController *)childController
there you can catch your childController e.g. by comparing its Class and assign it to a property / ivar
-(void)addChildViewController:(UIViewController *)childController
{
[super addChildViewController:childController];
if([childController isKindOfClass:[InstallViewController class]])
{
self.installViewController = (InstallViewController *)childController;
}
}
This will save your from iterating trough the childViewControllers.