Dynamically loading NIBs? - iphone

Rather than having three separate controllers and their associated *.xib files I am trying to setup a generic controller and then instantiate it with one of three different xib files RED.xib" "GREEN.xib" & "BLUE.xib"
NSString *nibColor;
switch (selectedRow) {
case 0:
nibColor = #"RED";
break;
case 1:
nibColor = #"GREEN";
break;
case 2:
nibColor = #"BLUE";
break;
}
ColorController *colorController = [[ColorController alloc] initWithNibName:nibColor bundle:nil];
My problem is that I am not linking the view and get the following error.
loaded the "RED" nib but the view outlet was not set.
I understand that normally you link the view in IB, but is there a way to dynamically pick the nib at runtime, or do I need to create separate redController, blueController and greenControllers?
cheers Gary

From Apple's UIViewController docs, which I'm assuming ColorController is a subclass of:
When you define a new subclass of UIViewController, you must specify the views to be managed by the controller. There are two mutually exclusive ways to specify these views: manually or using a nib file. If you specify the views manually, you must implement the loadView method and use it to assign a root view object to the view property. If you specify views using a nib file, you must not override loadView but should instead create a nib file in Interface Builder and then initialize your view controller object using the initWithNibName:bundle: method. Creating views using a nib file is often simpler because you can use the Interface Builder application to create and configure your views graphically (as opposed to programmatically). Both techniques have the same end result, however, which is to create the appropriate set of views and expose them through the view property.

Related

Strong IBOutlet nil in XIB with multiple views

Calling Bundle.main.loadNibNamed to load a .xib file which contains (n) multiple variants of one UI defined with multiple UIViews instantiates n instances of my subclass.
I then apply a filter expression to choose the correct variant with .first(where: { $0.restorationIdentifier == <correct restoration ID>.
In this instance my filter expression correctly returns the 5th UIView inside of my .xib but the #IBOutlets in my custom class are connected to the 1st UIView that was instantiated but which is immediately deprecated by what I assume is ARC.
This leads me to having unexpectedly nil IBOutlets. What can be done to connect the IBOutlets to the correct (5th in this case) UIView returned by Bundle.main.loadNibBaned
The problem is that loadNibNamed is instantiating all your views, and you're only choosing to keep some of them around. In the process, IB outlets are assigned in some order, which most likely doesn't end up with your desired object being assigned to an outlet last.
I don't think a nib file gives you a way to instantiate only some of its multiple top level objects. You need to either split your various views into multiple nibs (and only load the one you need), or switch to using a Storyboard, which does let you instantiate specific objects by their identifier.

What would be the appropriate way to use a delegate protocol from 2 views?

I have a view controller, which uses a delegate protocol in order to pass back an array of strings. I now have another view controller, which I'd like to use the same protocol, but I've I use it I get a warning in Xcode Duplicate protocol definition of 'SearchDetailsDelegate' is ignored.
I need these two views to pass back an array for the parent view controller to parse. What would be a more appropriate way of achieve what I need to do here? Would key value observing be the way to go here?
You have few options:
rename your protocols to be different.
create an external protocol and adopt that protocol on each view
Add a property to your view called ParentController with a type of it's parent.
#property (strong,nonatomic) ParentViewController *ParentController;
(synthesise that off course)
Then, in your viewController, when you instantiate the view assign the viewController as the parent
YourView *childView = [[YourView alloc]init];
childView.parentController = self;
Now you can add a method in your viewController that can receive the strings array
-(void)setStringsArray:(NSArray*)arr{
//do what ever you need with the array
//don't forget to add this method to your .h file so it will be visible
}
Lastly send the strings array from the view:
[self.parentController setStringsArray:yourArray];
BTW
if you want to know what view send the array you can:
-(void)setStringsArray:(NSArray*)arr fromView:(UIView*)senderView{
//do what ever you need with the array
//don't forget to add this method to your .h file so it will be visible
}
and use
[self.parentController setStringsArray:yourArray fromView:self];
BTW 2
an other option will be to use notifications.
Define the protocol in a separate .h file (new file of objective c protocol) and then include it in the required view controllers.Redefining the same protocol in two different view controllers is not recommended as it has been in your case

How to programmatically assign/replace an existing UITableView with a new one?

I have the following code to my UIViewController class implementation:
- (void)viewDidLoad {
CustomUITableView *customUITableView = [[CustomUITableView alloc] initWithFrame:self.view.frame];
[self.view addSubview:customUITableView];
[super viewDidLoad];
}
When I run the app, the table shows up just fine. There are two problems I have with this approach:
I lose the use of StoryBoard since the table is dynamically added to the view. I would like to arrange my view in StoryBoard and not have to manually tweak its position in code.
I also want the ability to swap between CustomUITableViews at runtime (if possible). For example, if I create a "CustomUITableView_Number2", can I simply decide which custom class I want to use for the table at runtime?
I know I can accomplish this in StoryBoard by assigning a custom class to the UITableView, but I want to see how far I can go using only code. My goal is to arrange the layout using StoryBoard, but assign the custom class I want at runtime.
Does anyone know if this is possible? Sample code would really help. Thank-you.
You could load a regular UITableView from interface builder and then initialize a CustomUITableView with the parameters you are interested in from the UITableView.
- (void)viewDidLoad {
//Your tableView was already loaded
CustomUITableView *customUITableView = [[CustomUITableView alloc] initWithFrame:tableView.frame style:tableView.style];
customUITableView.backgroundColor = tableView.backgroundColor;
//...
//... And any other properties you are interested in keeping
//...
[self.view addSubview:customUITableView];
[super viewDidLoad];
}
This approach will work if you make sure to specify each property you think you would modify in interface builder. Just make sure to remove the UITableView that was loaded via the nib after you copy the properties over.
sure you can replace the table.
something as simple as
Class MyTableClass = NSClassFromString(#"WhateverClass");
id newTableView = [[MyTableClass alloc] init];
[newTableView performSelector:#selector(someSel:)];
make sure you set your datasource and delegate and whatever properties from the original nib instantiated view first, then
you could then set your local tableview property to this new object and as long as the ivar has a proper cast you could call methods on it without resorting to runtime calls like above.
You can also dig into the obj-c runtime if you wanted to do it in a different way.
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ObjCRuntimeRef/Reference/reference.html

How to use a common target object to handle actions/outlets of multiple views?

I'm beginning to try and use IB where previously I used code. I have created nibs for iPad/iPhone Portrait/Landscape so I have four possible views. All have files owner set to UIViewController, all have an object added from the palette set to my NSObject subclass "TargetObject". No problem setting IBActions/IBOutlets, just ctrl-drag as usual to create the code in one nib, then go and right-click the object and drag the actions/outlets to the right controls in the other nibs so that the connection exists in all nibs. The idea is that the app delegate kicks off the appropriate view controller for the current device and then that view controller loads the right view depending on orientation.
(Maybe with autoresizing mastery this approach is not necessary - I want to know if it is possible to do it this way because getting the layouts to rearrange themselves just right just with those IB controls is very, very frustrating.)
The idea behind these one-to-many views and target objects is that I can use them anywhere in the app. For example, where I have something displayed which is an object in core data
and I would like to allow detail view/edit in multiple places in the app. It seems like a very MVC way to do things.
The problem comes in loadView of the view controller when I do:
NSArray *portraitViewArray = [[NSBundle mainBundle] loadNibNamed:#"Weekview_iPad_Portrait" owner:self options:nil];
portraitWeekView = [portraitViewArray objectAtIndex:0];
NSArray *landscapeViewArray = [[NSBundle mainBundle] loadNibNamed:#"Weekview_iPad_Landscape" owner:self options:nil];
landscapeWeekView = [landscapeViewArray objectAtIndex:0];
// this object is intended to be a common target for portrait and landscape arrays.
weekViewTarget = [portraitViewArray objectAtIndex:1];
UIInterfaceOrientation interfaceOrientation = self.interfaceOrientation;
if(interfaceOrientation == UIInterfaceOrientationPortrait || interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown)
self.view = portraitWeekView;
else
self.view = landscapeWeekView;
As you guessed, everything is fine in portrait orientation but in landscape any interaction with the target object crashes. The weekViewTarget object refers to an object created with the portrait view, so the landscape view is looking for a different instance that hasn't been held with a reference. In the landscape view _viewDelegate is nil - it is private so I can't fix it up in code.
I can hold onto the landscape target object with a reference too, but then there will be problems whenever the orientation changes - in case of any stateful controls like UITableView the datasource/delegate will change resulting in strange user experience on rotation. Unless I ensure the target object implements only simple actions and uses a separate, shared object to implement datasource/delegate objects for any table views or text views required.
One thing I have thought of is to make the target a singleton. Seems like a hack and here will be times that can't work.
What's the right way to create multiple views implemented as nibs using a common target object to do stuff to the model?
progress - things begin to work if I give each target a pointer to its own type:
WeekViewTarget *forwardTarget;
and let the real handler's forwardTarget = nil while the unused handler's forwardTarget is set as follows:
weekViewTarget = [portraitViewArray objectAtIndex:1];
forwardedWeekViewTarget = [landscapeViewArray objectAtIndex:1];
forwardedWeekViewTarget.forwardTarget = weekViewTarget;
and then do the following in each IBAction:
- (IBAction)timeBlockTapped:(id)sender {
if(forwardTarget != nil) {
[forwardTarget performSelector:#selector(timeBlockTapped:) withObject:sender];
} else {
NSLog(#"Got a tap event with sender %#", [sender description]);
}
}
Still doesn't feel right though.
Adam, it sounds like you want a proxy (aka external) object.
Your Problem
NSBundle and UINib return auto-released arrays when instantiating object graphs from NIBs. Any object you don't specifically retain will be released when the array is released at the end of the event loop. Obviously objects inside the nib that reference each other will be retained as long as their owner is retained.
So you 2nd instance of your action target object is being released because you're not hanging on to it.
What you really want is just a single instance of your target object that both NIBs can reference. So you can't have your NIB instantiate an instance of that object. But your NIB needs to be able to make reference to that object instance!
Oh dear, what to do?!
Solution: Proxy/External Objects
Basically an external object is a proxy you place in a NIB. You then feed in the actual instance that when you instantiate the NIB's object graph. The NIB loader replaces the proxy in the NIB with your externally provided instance and configures any targets or outlets you've assigned to the proxy in Interface Builder.
How To Do It
In Interface Builder drag in an "External Object" rather than NSObject instance. It will appear in the upper section along with File's Owner in Xcode 4.x.
Select it and in the "Identity Inspector" set it's class to the appropriate type.
In the "Attributes Inspector" set the Identifier string to "TargetObject" (or whatever string you want to use in the code below).
Profit!
The Code
id owner = self; // or whatever
id targetObject = self.targetObject; // or whatever...
// Construct a dictionary of objects with strings identifying them.
// These strings should match the Identifiers you specified in IB's
// Attribute Inspector.
NSDictionary *externalObjects = [NSDictionary dictionaryWithObjectsAndKeys:
targetObject, #"TargetObject",
nil];
// Load the NIBs ready for instantiation.
UINib *portraitViewNib = [UINib nibWithNibName:#"Weekview_iPad_Portrait" bundle:nil];
UINib *landscapeViewNib = [UINib nibWithNibName:#"Weekview_iPad_Landscape" bundle:nil];
// Set up the NIB options pointing to your external objects dictionary.
NSDictionary *nibOptions = [NSDictionary dictionaryWithObjectsAndKeys:
externalObjects, UINibExternalObjects,
nil];
// Instantiate the objects in the nib passing in the owner
// (coincidentally also a proxy in the NIB) and your options dictionary.
NSArray *portraitViewArray = [portraitViewNib instantiateWithOwner:owner
options:nibOptions];
NSArray *landscapeViewArray = [landscapeViewNib instantiateWithOwner:owner
options:nibOptions];
self.view = [(UIInterfaceOrientationIsPortrait(interfaceOrientation)) ? portraitViewArray : landscapeViewArray) objectAtIndex:0];
Notes
You're using NSBundle to load NIBs. You should be using UINib. It is much faster at reading the NIB files. Also, it separates the NIB loading from the object instantiation. Which means you can load the NIB in your init, awakeFromNib, or viewDidLoad and not instantiate the contents of the NIB. Then at the last possible moment when you need the actual objects in the NIB to you can call instantiateWithOwner.
Say in the case of using a UITableViewController you would load your table cell nibs in viewDidLoad but not actually instantiate them until you needed a cell of that type in -tableView:cellForRowAtIndexPath:.
On the basis of interfaceOrientation you can identify the orientation and reset the frame/s of controls in xib.
For more, this will guide you...

Adding item to a subview from a external class

I've created a UIView and can add things to it by using [self.topView addSubview:image]; Now I am importing a class to create a calendar which has a heap of buttons. I could put it in the same class and say [self.topView addSubview:button] but if its in another class how do I add it to the subview of the class that owns it? Hope that makes sense...
You need a reference in your external class to the class that owns the view (call it the "owner class"), and presumably write a method in your owner class to add a passed-in view to your chosen subview. Something along the lines of:
- (void) insertSubview:(UIView*)newView {
if (newView) [self.topView addSubview:newView];
}
Setting up the reference can be done a number of ways, so I'll leave that one to you.