Using id<protocol> for file's owner in Interface Builder? - objective-c

I have a custom UITableViewCell that I am instantiating from a nib using instantiateWithOwner:(id)owner options:(NSDictionary *)options. When the nib is instantiated, I am saving it to an IBOutlet defined in my view controller, which is set as the file's owner in the .xib file. Everything's been working great.
I've now come across the need to use this custom cell in multiple view controllers. I was hoping that I could define a protocol (e.g. CustomCellOwner), which multiple view controllers could implement. The protocol would simply define the IBOutlet used to reference the cell when instantiated.
So ideally, I would like to set "file's owner" to:
id <CustomCellOwner>
in Interface Builder.
However, Interface Builder only seems to allow you to set file's owner to a known class, not to an id implementing a protocol?
Is there any way to do this? Or, a simpler way to approach this problem?
Thanks!

This isn't the solution you're asking for, but you could make a UIViewController subclass that you subclass for each view controller that needs to use your nib. Something like:
#interface CustomCellOwnerViewController : UIViewController
#property (nonatomic, strong) IBOutlet UIButton *someButton;
-(IBAction)doSomething;
#end
And then use that as the base class for each:
#interface FirstView : CustomCellOwnerViewController
Then you could simply set File's Owner to CustomCellOwnerViewController with no problems.
Just an idea.

I ran into this today and didn't find a good solution. I did however hack it so that it seems to work ok. It definitely feels like a hack though.
First I created a "fakeOwner" class like this:
#interface fakeOwner : NSObject
#property (nonatomic, assign) IBOutlet MyBaseCell* itemTableCell;
#end
#implementation fakeOwner
#synthesize itemTableCell;
#end
I then set the object's owner in the XIB as fakeOwner and connected the outlet. Then for each controller that wants to use these cells I add the same property and create the class like this:
[[NSBundle mainBundle] loadNibNamed:#"MyBaseCell" owner:self options:nil];
MyBaseCell* itemCell = self.itemTableCell;
self.itemTableCell = nil;
Since the fakeOwner and my controller have the same IBOutlet, loading the cell with the controller as the owner causes the connection to happen even though that isn't what is explicitly set in the XIB.
Not 100% if the memory management is right currently (I think it's ok), but other than that it seems to work great. I would love to see a better way of doing this though.

Making a fake owner will work; however, such a solution may be fragile and inextensible. In a sense, the cell owns itself, but even that is technically incorrect. The truth is that UITableViewCells do not have owners.
The proper way to implement a custom table view cells is to first create a custom subclass of UITableViewCell. In this class you will define all of the IBOutlets and such for the cell. Here is a sample of a header file:
#interface RBPersonCell : UITableViewCell
#property (nonatomic, strong) IBOutlet UILabel * nameLabel;
#property (nonatomic, strong) IBOutlet UILabel * ageLabel;
- (void)setupWithPerson:(Person *)person;
#end
From there, I have a convenience method that creates the cell from the nib, if necessary:
+ (id)cellForTableView:(UITableView *)tableView reuseIdentifier:(NSString *)reuseID fromNib:(UINib *)nib {
if (!reuseID)
reuseID = [self cellIdentifier];
id cell = [tableView dequeueReusableCellWithIdentifier:reuseID];
if (!cell) {
NSArray * nibObjects = [nib instantiateWithOwner:nil options:nil];
// Sanity check.
NSAssert2(([nibObjects count] > 0) &&
[[nibObjects objectAtIndex:0] isKindOfClass:[self class]],
#"Nib '%#' does not appear to contain a valid %#",
[self nibName], NSStringFromClass([self class]));
cell = [nibObjects objectAtIndex:0];
}
return cell;
}
This method encapsulates all of the creation code so I never have to see it or rewrite it. It assumes that the custom cell is the first root view in the nib. This is a fairly safe assumption since you should only have the custom cell as a root view.
With all this code in place, you are ready to work in Interface Builder. You first need to set the custom class in the identity inspect. Next, don't forget to set your cell identifier. For convenience, it's best to use the name of the custom class. When you drag your connections, rather than drag them to File's Owner, drag your connections to the custom cell itself.
Most of what I have learned about custom table view cells comes from iOS Recipes recipes 15-16. Here is a free extract directly from The Pragmatic Bookshelf. You can check out that book for more details.
EDIT:
I finally got around to open sourcing my RBSmartTableViewCell class. You can find it on my GitHub. You should find this class more useful than the code directly from iOS Recipes, since my class treats all cells the same, regardless of whether they are constructed using XIBs, UIStoryboard, or code. This repo also includes working samples.

In iOS 5.0 there is now the registerNib:forCellReuseIdentifier: method on UITableView which I believe attempts to solve a similar problem.
From the documentation:
When you register a nib object with the table view and later call the dequeueReusableCellWithIdentifier: method, passing in the registered identifier, the table view instantiates the cell from the nib object if it is not already in the reuse queue.
This could be an alternative approach depending on your requirements.

Another option might be to create a lightweight 'factory' object that handles the creation of the cells for you. This object would be the FilesOwner in interface builder, with the rootObject outlet set appropriately.
#interface NibLoader : NSObject
#property (nonatomic, strong) UINib * nib;
#property (nonatomic, strong) IBOutlet id rootObject;
- (id)initWithNibName:(NSString *)name bundle:(NSBundle *)bundleOrNil;
- (id)instantiateRootObject;
#end
#implementation NibLoader
#synthesize nib, rootObject;
- (id)initWithNibName:(NSString *)name bundle:(NSBundle *)bundleOrNil {
self = [super init];
if (self) {
self.nib = [UINib nibWithNibName:name bundle:bundleOrNil];
}
return self;
}
- (id)instantiateRootObject {
self.rootObject = nil;
[self.nib instantiateWithOwner:self options:nil];
NSAssert(self.rootObject != nil, #"NibLoader: Nib did not set rootObject.");
return self.rootObject;
}
#end
Then in the view controllers:
NibLoader *customCellLoader = [[NibLoader alloc] initWithNibName:#"CustomCell" bundle:nil];
self.customCell = customCellLoader.instantiateRootObject;
I prefer explicitly setting the root object instead of searching through the array returned from instantiateWithOwner:options: because I know the position of the objects in this array has changed in the past.

Related

IBOutlet is nil after initWithCoder is called

Simple problem, I have defined a UIImageView, called bigImageView in a UIViewController using the storyboard,
It's declared in the h file of that UIViewController as follows:
#property (retain, nonatomic) IBOutlet UIImageView *bigImageView;
on my appDelegate I init the UIViewController as follows:
imageViewController = [storyboard instantiateViewControllerWithIdentifier:#"chosenImageController"];
this calls initWithCoder on my UIViewController m file:
-(id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Custom initialization
}
return self;
}
This function is only called once so there's no double init.
However, later, when I check my bigImageView pointer, it's still nil.
Isn't the init supposed to allocate memory to it?
I think that's why when I try to set this UIImageview to hold a UIImage it doesn't display the image
Thanks
It's all working how it's meant to. First every object in the nib/storyboard gets alloc/init called on them, then all the connections are made, and then viewDidLoad is called.
You need to wait for - (void)viewDidLoad to be called on your controller, and then bigImageView should be set. If it's not set then you did something wrong in the storyboard.
init methods are not responsible for allocating any memory. All memory is allocated by the alloc method which is always called before init. Alloc will fill all your instance variables with nil/NULL/0 values, and then init gives the chance to assign initial values to each one (based on the contents of the NSCoder object usually, but it's up to you to decide what should be done).
For IB outlets however, those are setup by the nib loading process after init.
EDIT:
// ViewControllerA.m:
imageViewController = [storyboard instantiateViewControllerWithIdentifier:#"chosenImageController"];
imageViewController.image = imageToShow;
// ViewControllerB.h
#property (retain) NSImage *image;
#property (retain, nonatomic) IBOutlet UIImageView *bigImageView;
// ViewControllerB.m
- (void)viewDidLoad
{
self.bigImageView.image = self.image;
[super viewDidLoad];
}
You don't need to define initWithCoder, since you have no custom logic in there. I would delete that boilerplate code.
Here is what I would check:
In the storyboard, ensure that the class of the view controller is set properly.
Ensure that the outlet is hooked up properly in the storyboard by looking for a circle near your #property. It should be a filled in circle, not an outline of a circle.
Make sure you are reading the value only after viewDidLoad is called. Apple's only guarantee is that the outlet is set after this method call.
Update: It sounds like you want to access the image view before the view is loaded. There is no way to do this. One hack is to call viewController.view which will force the view to load, but there are many reasons why you should not do this.
A better approach would be to implement properties on your view controller which work for both when the view is not loaded and when the view is loaded. You can see an example of an elegant solution in this question. Notice how if the view is loaded, the photographerLabel will get set via the didSet method. On the other hand, if the view is not loaded, it will get set via the viewDidLoad method. For an Objective-C version of that code or for more details, see the linked video in that question.

NSLogging UILabel's text outputs null

I have a custom view controller called TimerViewController and subclasses of this called FirstViewController and FourthViewController.
I declared an instance of FirstViewController in FirstViewController's .h named controller.
In the viewDidLoad method of FourthViewController's .m, I have:
controller = [self.storyboard instantiateViewControllerWithIdentifier:#"mainController"];
In the storyboard, I've declared the view controller ID as mainController and declared a custom class of FourthViewController. Then, in FourthViewController's .m I have:
controller.mainLab.text = [NSMutableString stringWithFormat:#"This is a string"];
NSLog(#"%#", controller.mainLab.text);
This, however, outputs (null).
Why does this happen?
mainLab must be nil. So your outlet probably isn't connected to your XIB.
As an aside, using stringWithFormat: with a string that isn't a format is wasteful.
Your neglecting to tell us something about the rest of your project, im just not sure what it is.
I fired up Xcode just to run through this real quick and the process is simple.
Drag UI Label to your XIB
Control click from label to your .h
For testing I did
#import "SOViewController.h"
- (void)viewDidLoad
{
[super viewDidLoad];
self.mainLabel.text = #"This is my label";
NSLog(#"%#", self.mainLabel.text);
}
My .h looks like this:
#import <UIKit/UIKit.h>
#interface SOViewController : UIViewController
#property (weak, nonatomic) IBOutlet UILabel *mainLabel;
#end
Is this part of a custom class? Is there something else going on? If its a vanilla label it should work without issue using the aforementioned code.
it looks like your mainLab has not yet been created. When you call methods on nil objects, the method automatically returns nil. Make sure you actually create the label before running this line of code.
You can't access the label (or any other UI element) of another controller right after you instantiate it, because its viewDidLoad method has not yet run. If you want to set the text of a label in another controller, you have to pass the text to that controller, and have it set the text on the label in its viewDidLoad method. So instead of the this:
controller = [self.storyboard instantiateViewControllerWithIdentifier:#"mainController"];
controller.mainLab.text = [NSMutableString stringWithFormat:#"This is a string"];
You need to do this:
controller = [self.storyboard instantiateViewControllerWithIdentifier:#"mainController"];
controller.mainLabText = #"This is a string";
where mainLabText is a string property you create in FourthViewController. Then populate the label in FourthViewController's viewDidLoad:
self.mainLab.text = self.mainLabText;

Loading XIB file without a UIViewController

I'd like to design a UIView and some sub-views (UIWebView, UIToolbar, some UIBarButtonItems, a progress indicator and so-forth) using the Interface Builder, but I think it's unnecessary to do this traditionally, by using a UIViewController, using presentViewController:animated etc.
So, I created a custom class, with the .h file code as follows:
#interface FileInteractionManager : NSObject {
}
#property (nonatomic, retain) IBOutlet UIView *fileView;
#property (nonatomic, retain) IBOutlet UIWebView *fileWebView;
#property (nonatomic, retain) IBOutlet UIBarButtonItem *printButton;
#property (nonatomic, retain) IBOutlet UIBarButtonItem *optionsButton;
#property (nonatomic, retain) IBOutlet UIBarButtonItem *doneButton;
My .m file is as follows:
#implementation FileInteractionManager
#synthesize fileView, fileWebView, doneButton, optionsButton, printButton;
-(id)init {
NSArray *array = [[NSBundle mainBundle] loadNibNamed:#"FileInteractionView" owner:self options:nil];
NSLog(#"Load success!");
return self;
}
Finally, I create a stand-alone xib file named 'FileInteractionView.xib', change the file's owner to the custom class I created above, and wire up the IBOutlets.
When I call the init method on my class, I can see in the debugger that all my IBOutlet objects are instantiated properly.
My questions are:
Is the loadNibNamed:owner:options: method the right way to load my stand-alone .xib file? I don't like the fact that this method returns an array I have no use for (the top-level object returned matches my variable fileView, but I've already linked them through the Interface Builder).
Is my general approach correct in solving my problem? I carried out the above steps because I wanted a simple UIView object that I could add to my existing UIViewController, rather than present and dismiss a whole new UIViewController.
I use a little different approach. I create a subclass of UIView (MyCustomView i.e.) then the xib with the UI of the view and change the (main) view class the the one just defined. In the xib then you can link the outlet to the custom view itself (not the file owner).
Finally in the class definition I create a function like this:
+ (id) newFromNib
{
NSArray *nibArray = [[UINib nibWithNibName:NSStringFromClass([self class]) bundle:nil] instantiateWithOwner:nil options:nil];
return nibArray[0];
}
Just a couple of notes:
1) this's a class method, you can use "self" just for stuff like "NSStringFromClass([self class])" but the real object is the variable returned
2) this example suppose the xib have the same name of the class (via NSStringFromClass([self class]) so I can copy-paste it without changing anything ;) ) and that your view is the first one defined in the xib (the standard). If you store more than a "main" view inside one xib pick the right element.
so where I need MyCustomView I do something like:
MyCustomView* mycv = [MyCustomView newFromNib];
then set frame/center and add to superview...
I think this way is pretty usefull if you have a "library" of complex UI elements and want to design them via xib then add when needed.
il Malvagio Dottor Prosciutto answer is nice. Here is a possible alternative.
Load nib in NS_DESIGNATED_INITIALIZER and become owner of subview
If we accept the xib to only hold a subview instead of the view itself, then we can load the subview in initWithFrame: and keep an ownership construction in xib.
#interface MyCustomView ()
#property (strong, nonatomic) IBOutlet UIView *subview;
#end
#implementation MyCustomView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil];
[self addSubview:self.subview];
return self;
}
#end

Setting a property from another property's setter from segue Objective-C

For reference, I'm trying to learn Objective-C through the Stanford iTunes-U course. I wanted to update one property from the setter of another (they are inherently connected. Also, is that bad style?). The property I am trying to update is a UILabel, but it doesn't work like I thought.
This code is in one of my view controllers:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
[segue.destinationViewController setProgram:self.brain.program];
}
Naturally, this code calls the setter for the Program property of the incoming viewController. Here's that setter code:
-(void)setProgram:(id)program {
_program = program;
self.title = [CalculatorBrain descriptionOfProgram:program];
[self.graphview setNeedsDisplay];
self.testLabel.text = #"Trying to update your text!";
}
and the header file:
#import <UIKit/UIKit.h>
#interface GraphViewController : UIViewController
#property (nonatomic, strong) id program;
#property (weak, nonatomic) IBOutlet UILabel *testLabel;
#end
Now, when this code is run, as the segue happens, the new view DOES have its title changed (the self.title line works). However, the UILabel DOES NOT get updated. Furthermore, if I call the setter again say in viewDidLoad, it does change the UILabel. Is this because of self being updated? What's going on here?
In one of the lectures, the professor explains that outlets aren't set yet in prepareForSegue. This means testLabel is nil, and you're sending a message to nil (which is allowed and doesn't crash your app). To solve your problem, set the textLabel's text in viewDidLoad or viewWillAppear.
I believe this is "fixed" in iOS 6, but it won't be backwards compatible, so if you want to support iOS 5 still, you'll have to use viewDidLoad or viewWillAppear to update outlets (which I think is better to do anyway).

iOS Child Views, SRP and custom events

I'm kind of new to iOS development and need some advice. I have a chat like app. The UI should have a child view for posting new messages to the server and one child view for viewing messages in a table view.
I've build both child views in Interface Builder as XIB:s. But I'm not sure how to use these on the main view controller. Can I use IB to add my custom views to the design surface? Or do I need to add these programmatically?
What is the best way to send messages or custom events between these two child views? I'd like to have them as decoupled as possible. Mostly I'd like to send an event when the user log on or off, so that the UI can react to these changes. I'd also like the table view with messages to know when a new messages is posted from the write view.
// Johan
In order to get the contents of a xib file you've got to load it first sending loadNibNamed:owner:options: message to NSBundle class.
Consider you have a UIView subclass named CustomView and CustomView.xib file. In the xib file each view has a tag. Your .h file would look like:
#interface CustomView : UIView
#property (nonatomic, assign) UILabel *someTextLabel; //use assign in order to not to override dealloc method
#end
.m
#implementation CustomView
- (id)init {
self = [super init];
if (self) {
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomView" owner:nil options:nil];
[self addSubview:[topLevelObjects objectAtIndex:0]]; //this object is a CustomView.xib view
self.someTextLabel = (UILabel *)[self viewWithTag:5]; //consider you have a UILabel on CustomView.xib that has its tag set to 5
}
return self;
}
#end
This is about how to use .xibs for your custom UIView subclasses. If your app is like a chat then you'll have to add them programmatically.
As for the best way to send messages between two custom views, you'll have to create a weak reference to each other in each of them.
in one
#property (nonatomic, assign) CustomView *customView;
in another
#property (nonatomic, assign) AnotherCustomView *anotherCustomView;
and just send them messages when some even happens
- (void)buttonPressed {
[customView handleButtonPressedEvent];
}
Let me know if this is clear.