Using id for file's owner in Interfa

2019-04-22 04:05发布

问题:

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!

回答1:

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.



回答2:

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.



回答3:

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.



回答4:

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.



回答5:

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.