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.
Related
I'm new to Objective-C and have a question. Did the search multiple times but I couldn't find what I was looking for.
I'm using storyboard for this app. On the homescreen you've got some buttons with labels above them. Those labels should tell a number. When pushing the button you go to a new viewController where you have input that (after 'save') goes back to the homescreen and updates the label with the correct number. All that works great for one button and I'm very happy about it.
The problems are:
1. Since I have multiple buttons with labels, I want to use the same viewController to give input over and over again. I tried connecting every button to slide to the viewController under the identifier "AddData", but Xcode doesn't allow the same identifiers twice or more in storyboard. So I would need something else for this. Any idea?
2. Currently I use the following code to bring back the data to the homescreen:
homeScreenViewController
- (IBAction)unwindToHomeScreen:(UIStoryboardSegue *)segue;
{
inputDataViewController *source = [segue sourceViewController];
self.logoOneLabel.text = source.endTotalNumber;
}
inputDataViewController:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if (sender != self.saveButton) {
return;
} else {
if (endTotalLabelNumber > 0) {
self.endTotalNumber = [NSString stringWithFormat:#"%.0f", totalLabelNumber + endTotalLabelNumber];
} else if (endTotalLabelNumber == 0 && totalLabelNumber == 0){
self.endTotalNumber = 0;
} else {
self.endTotalNumber = [NSString stringWithFormat:#"%.0f", totalLabelNumber + endTotalLabelNumber];
}
}
}
This works great for the one button, but how to use this with multiple? I heard about Delegates to use the same viewController multiple time and get data back to different places, but I just don't get it. Any help?
You shouldn't need delegates.
What you will need is a property on the view controller that handles input to it knows which button it is handling input for.
When you segue to the input controller, set this property, based on which button was pushed. When you unwind back, fetch this property to know which label to modify.
For example, in your input view controller's .h file, add a property like this:
#property (nonatomic,assign) NSInteger handlingTag;
Or something, whatever name makes sense to you.
Now you need to implement your home screen view controller's prepareForSegue:sender:.
Use the sender argument to determine which button was pushed, and based on that, set the input view controller's new handlingTag property based on the button in a way that you will know what to do with it when we unwind.
Now in the unwind method:
switch (source.handlingTag)
Create a switch structure based on the source's handlingTag property, and set the appropriate label based on this value.
As Jeff points out in the comments, it'd be a really good idea to define an NS_ENUM to use here for the property rather than an NSInteger. The NS_ENUM would allow you to name the values you're using.
There is a few different way to implement what you need. But i think most common its a delegate.
This is how your inputDataViewController looks like:
#import <UIKit/UIKit.h>
#protocol inputDataDelegate;
#interface inputDataViewController : UIViewController
#property (weak) id<inputDataDelegate> delegate;
#property (strong, nonatomic) NSNumber *buttonTag;
#end
#protocol inputDataDelegate <NSObject>
-(void) inputDataViewControllerDismissed:(id)data;
#end
Then in #implementation, you should in "save" button action, message to you delegate method :
[self inputDataViewControllerDismissed:#{#"buttonTag":buttonTag,#"endTotalNumber":endTotalNumber}
Next in homeScreenViewController connect delegate :
#interface homeScreenViewController : UIViewController<inputDataDelegate>
After that in #implementation:
-(void)inputDataViewControllerDismissed:(id)data
{
// if you use modal
[self dismissViewControllerAnimated:YES completion:nil];
// or if you use push
//[self.navigationController popViewControllerAnimated:YES];
switch (data[#"buttonTag"]) {
case 1:
self.lableWtiTagOne = data[#"endTotalNumber"];
break;
case 2:
self.lableWtiTagTwo = data[#"endTotalNumber"];
break;
// number of cases depend how many buttons you have
}
Also, most important, thing didn't forget send self to our delegate:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"inputDataController"])
{
inputDataViewController *inputCtrl = [segue destinationViewController];
inputCtrl.delegate = self;
inputCtrl.buttonTag = sender.tag
}
}
Let's say that I have view controller Origin and Destination. I would like to declare something like:
//origin.m file
-(void)pushNextView {
self.conditional = YES;
[self performSegueWithIdentifier:#"toDestination" sender:self];
}
Where I have set my conditional as:
//origin .h file
#propery BOOL conditional;
Now in my Destination view controller I'd like to set a conditional based on the property that I've set in my origin:
// destination .m file
#import "OriginViewController.h"
OriginViewController *origin = [OriginViewController alloc] init];
if (origin.conditional == YES){
self.navigationItem.hidesbackbutton = YES;
}else{
// Do Nothing
}
for some reason this conditional statement does not work. Does this have to do anything with storyboards?
With the setup you seem to have it would be most easy to do this. You could directly access destination.hidesbackbutton when executing the segue:
//in origin.m
-(void) prepareForSegue.... {
if ([segue.identifier isEqualToString: #"identifierString"]) {
DestinationVC *destination = (DestinationVC*)[segue destinationViewController];
destination.hidesbackbutton = self.conditional; //you can set the .hidesbackbutton property here directly, no need for another property, if your setup is just as simple as in the given example
}
}
Like this, the destination doesn't check the origin's state and then set it's state, instead the origin just sets the destination's state.
You can do it both ways, but this way is more common.
Of course hidesbackbutton has to be a public property declared in DestinationVC .h file.
And as already mentioned, it should really be hidesBackbutton Or hidesBackButton.
(This all assumes that the class of your DestinationViewController is called DestinationVC)
Two things: hidesbackbutton looks like there's some camelCase missing. This should at least give a warning, doesn't it?
Also from an architectural point of view, I would not ask for the condition. Pass the state to the destination view controller and implement a BOOL variable there.
Draft:
// origin.m
- (void)prepareForSegue...
{
if([segue.identifier isEqualToString:"yourIdentifier"]) {
[(XYZViewController *)segue.destinationViewController setConditional:YES];
}
}
you should pass the state from your main controller to destination controller where you should handle this View state like and you have to define
//destination.h file
#propery BOOL conditional;
so when you push the controller from main you can set the destination controller view state in
- (void)viewDidLoad
{
[super viewDidLoad];
if (origin.conditional == YES) {
self.navigationItem.hidesBackButton = YES;
} else {
}
}
I have a container view, which uses a storyboard embed segue to load an embedded static table view. The segue ID is 'CONTAINER'.
When I run the following code, the prepareForSegue never actually gets called so no data is passed from the parent to the child.
- (void)viewDidLoad {
[super viewDidLoad];
if ([sGender isEqualToString:#"MALE"]) {
containerGender = #"MALE";
if ([containerGender isEqualToString:#"MALE"]){
NSLog(#"MALE");
}else{
NSLog(#"BROKEN");
}
}
else if ([sGender isEqualToString:#"FEMALE"]) {
containerGender = #"FEMALE";
}
}
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"CONTAINER"]) {
if([containerGender isEqualToString:#"MALE"]) {
if ([containerGender isEqualToString:#"MALE"]){
NSLog(#"MALE");
}else{
NSLog(#"BROKEN");
}
SportTableViewController *tableView = segue.destinationViewController;
tableView.sportGender = #"MALE";
}
else if ([containerGender isEqualToString:#"FEMALE"]){
SportTableViewController *tableView = segue.destinationViewController;
tableView.sportGender = #"FEMALE";
}
}
}
My question is:
a)Why is prepareForSegue not called? Does the Storyboard Embed Segue behave differently to a standard seque?
b)Is there a better way of passing data from the container view to the embedded table?
Also please ignore the messy implementation/various log tests, just my attempts to work out whats going wrong.
My question actually stems from a misunderstanding of how container views load their embedded views. Apparently, it all happens before viewDidLoad. That means my conversion of sGender into containerGender took place too late.
I fixed it by passing sGender to the embedded view directly. I had thought I'd already tried this, but yesterday was obviously a slow day ;).
you can do it..
try
in childViewController
set-
#property(nonatomic,retain)parentViewController *parent;
and
in ParentViewController set-
ChildViewController *child = [[childviewController alloc]init];
child.parent=self;
It will set your parent class in childview and you can get data through parent object in child view controller.
i'm used to programming for iOS, and I've become very accustomed to the UIViewController. Now, i'm creating an OSX application and i'm having a few general questions on best practice.
In a UIViewController I generally setup my views in the -(void)viewDidLoad method - I don't actually create a custom UIView for the UIViewController unless it's really needed - so the UIViewController adds view to its own view, removes them, animates them and so forth - first off, is good practice?
And for my main question - what is the best practice in OSX? I like creating interfaces programatically and simply prefer it that way. If i, say create a new custom window and want to manage its view. What's the best way to do it, and where to i instantiate the user interface best?
Summary: How do i construct custom views programatically and set up a best-practice relationship between views and controllers in OSX? And is it considered good practice to use a view controller to create the views within its view?
Kind regards
To construct the view in code in an NSViewController, override loadView and be sure to set the view variable. Do not call super's implementation as it will attempt to load a nib from the nibName and nibBundle properties of the NSViewController.
-(void)loadView
{
self.view = [[NSView alloc] init];
//Add buttons, fields, tables, whatnot
}
For a NSWindowController, the procedure is very similar. You should call windowDidLoad at the end of your implementation of loadWindow. Also the window controller does not call loadWindow if the window is nil, so you will need to invoke it during init. NSWindowController seems to assume you will create the window in code before creating the controller except when loading from a nib.
- (id)initWithDocument:(FFDocument *)document
url:(NSURL *)url
{
self = [super init];
if (self)
{
[self loadWindow];
}
return self;
}
- (void)loadWindow
{
self.window = [[NSWindow alloc] init];
//Content view comes from a view controller
MyViewController * viewController = [[MyViewController alloc] init];
[self.window setContentView:viewController.view];
//Your viewController variable is about to go out of scope at this point. You may want to create a property in the WindowController to store it.
[self windowDidLoad];
}
Some optional fancification (10.9 and earlier)
Prior to 10.10, NSViewControllers were not in the first responder chain in OSX. The menu will automatically enable/disable menu items for you when an item is present in the responder chain. You may want to create your own subclass of NSView with an NSViewController property to allow it to add the controller to the responder chain.
-(void)setViewController:(NSViewController *)newController
{
if (viewController)
{
NSResponder *controllerNextResponder = [viewController nextResponder];
[super setNextResponder:controllerNextResponder];
[viewController setNextResponder:nil];
}
viewController = newController;
if (newController)
{
NSResponder *ownNextResponder = [self nextResponder];
[super setNextResponder: viewController];
[viewController setNextResponder:ownNextResponder];
}
}
- (void)setNextResponder:(NSResponder *)newNextResponder
{
if (viewController)
{
[viewController setNextResponder:newNextResponder];
return;
}
[super setNextResponder:newNextResponder];
}
Finally, I use a custom NSViewController that overrides setView to set the viewController property when I use my custom views.
-(void)setView:(NSView *)view
{
[super setView:view];
SEL setViewController = #selector(setViewController:);
if ([view respondsToSelector:setViewController])
{
[view performSelector:setViewController withObject:self];
}
}
- (BOOL)acceptsFirstResponder
{
return YES;
}
And for my main question - what is the best practice in OSX? I like
creating interfaces programatically and simply prefer it that way. If
i, say create a new custom window and want to manage its view. What's
the best way to do it, and where to i instantiate the user interface
best?
All these are done in awakeFromNib and init.
The following code is creating many windows and storing them in array. For each window you can add views. And each view may contains all the controls you wish to have.
self.myWindow= [[NSWindow alloc] initWithContentRect:NSMakeRect(100,100,300,300)
styleMask:NSTitledWindowMask
backing:NSBackingStoreBuffered
defer:NO];
[self.myWindowArray addObject:self.myWindow];
for (NSWindow *win in self.myWindowArray) {
[win makeKeyAndOrderFront:nil];
}
I am currently designing the structure for my first iPhone game and ran into a problem. Currently, I have a 'MenuViewController' that allows you to pick the level to play and a 'LevelViewController' where the level is played.
A UIButton on the 'MenuViewController' triggers a modal segue to the 'LevelViewController'.
A UIButton on the 'LevelViewController' triggers the following method to return to the 'MenuViewController':
-(IBAction)back:(id)sender //complete
{
[self dismissModalViewControllerAnimated:YES];
}
The problem is, I have a UILabel on the menu page that prints the number of total points a player has. Whenever I go back to the menu from the level, I want this label to automatically update. Currently, the label is defined programmatically in the 'MenuViewController':
-(void)viewDidLoad {
[super viewDidLoad];
CGRect pointsFrame = CGRectMake(100,45,120,20);
UILabel *pointsLabel = [[UILabel alloc] initWithFrame:pointsFrame];
[pointsLabel setText:[NSString stringWithFormat:#"Points: %i", self.playerPoints]];
[self.pointsLabel setTag:-100]; //pointsLabel tag is -100 for id purposes
}
self.playerPoints is an integer property of MenuViewController
Is there a way I could update the label? Thanks ahead of time!
This is a perfect case for delegation. When the LevelViewController is done, it needs to fire off a delegate method which is handled in the MenuViewController. This delegate method should dismiss the modal VC and then do whatever else you need it to do. The presenting VC should normally handled the dismissal of modal views it presents.
Here is a basic example of how to implement this:
LevelViewController.h (Above the Interface declaration):
#protocol LevelViewControllerDelegate
-(void)finishedDoingMyThing:(NSString *)labelString;
#end
Same file inside ivar section:
__unsafe_unretained id <LevelViewControllerDelegate> _delegate;
Same File below ivar section:
#property (nonatomic, assign) id <LevelViewControllerDelegate> delegate;
In LevelViewController.m file:
#synthesize delegate = _delegate;
Now in the MenuViewController.h, #import "LevelViewController.h" and declare yourself as a delegate for the LevelViewControllerDelegate:
#interface MenuViewController : UIViewController <LevelViewControllerDelegate>
Now inside MenuViewController.m implement the delegate method:
-(void)finishedDoingMyThing:(NSString *)labelString {
[self dismissModalViewControllerAnimated:YES];
self.pointsLabel.text = labelString;
}
And then make sure to set yourself as the delegate for the LevelViewController before presenting the modal VC:
lvc.delegate = self; // Or whatever you have called your instance of LevelViewController
Lastly, when you are done with what you need to do inside the LevelViewController just call this:
[_delegate finishedDoingMyThing:#"MyStringToPassBack"];
If this doesn't make sense, holler and I can try to help you understand.
Make a property self.pointsLabel that points to the UILabel, then you can just call something like [self.pointsLabel setText:[NSString stringWithFormat:#"Points: %i", self.playerPoints]]; to update the label with the new score
In your modal view header file, add the property:
#property (nonatomic,assign) BOOL updated;
Then in your main view controller, use didViewAppear with something like:
-(void)viewDidAppear:(BOOL)animated{
if (modalView.updated == YES) {
// Do stuff
modalView.updated = NO;
}
}
Where "modalView" is the name of that UIViewController that you probably alloc/init there.
Add more properties if you want to pass more info, like what level the user picked.