views:

52

answers:

2

I want to create a reusable component (a custom control) for the iPhone. It consists of several standard controls prearranged on a View, and then some associated code. My goals are:

  1. I want to be able to use Interface Builder to lay out the subviews in my custom control;
  2. I want to somehow package the whole thing up so that I can then fairly easily drag and drop the resulting custom component into other Views, without having to manually rewire a bunch of outlets and so on. (A little manual rewiring is fine, I just don't want to do tons and tons of it.)

Let me be more concrete, and tell you specifically what my control is supposed to do. In my app, I sometimes need to hit a web service to validate data that the user has entered. While waiting for a reply from the web service, I want to display a spinner (an activity indicator). If the web services replies with a success code, I want to display a "success" checkmark. If the web service replies with an error code, I want to display an error icon and an error message.

The single-use way to do this is pretty easy: I just create a UIView that contains a UIActivityIndicatorView, two UIImages (one for the success icon and one for the error icon), and a UILabel for the error message. Here's a screenshot, with the relevant parts marked in red:

alt text

I then wire up the pieces to outlets, and I put some code in my controller.

But how do I package up those pieces -- the code and the little collection of views -- so that I can reuse them? Here are a few things I found that get me partway there, but aren't that great:

  • I can drag the collection of views and controls into the Custom Objects section of the Library; then, later, I can drag them back out onto other views. But (a) it forgets which images were associated with the two UIImages, (b) there is a lot of manual rewiring of four or five outlets, and (c) most importantly, this doesn't do bring along the code. (Perhaps there's an easy way to wire up the code?)
  • I think I could create an IBPlugin; not sure if that would help, and it seems like a lot of work, and also it's not entirely clear to me whether IBPlugins work for iPhone development.
  • I thought, "Hmm, there's code associated with this -- that smells like a controller," so I tried creating a custom controller (e.g. WebServiceValidatorController) with associated XIB file. That actually feels really promising, but then at that point I can't figure out how, in Interface Builder, to drag this component onto other views. The WebServiceValidatorController is a controller, not a view, so I can drag it into a Document Window, but not into a view.

I have a feeling I'm missing something obvious...

+1  A: 

I've created similar constructs except that I do not use the result in IB, but instantiate using the code. I'll describe how that works, and at the end I'll give you a hint how that can be used to accomplish what you're after.

I start from an empty XIB file where I add one custom view at the top level. I configure that custom view to be my class. Below in view hierarchy I create and configure subviews as required.

I create all IBOutlets in my custom-view class, and connect them there. In this exercise I ignore the "File's owner" completely.

Now, when I need to create the view (usually in controller as part of while/for-loop to create as much of them as needed), I use NSBundle's functionality like this:

- (void)viewDidLoad
{
    CGRect fooBarViewFrame = CGRectMake(0, 0, self.view.bounds.size.width, FOOBARVIEW_HEIGHT);
    for (MBSomeData *data in self.dataArray) {
        FooBarView *fooBarView = [self loadFooBarViewForData:data];
        fooBarView.frame = fooBarViewFrame;
        [self.view addSubview:fooBarView];

        fooBarViewFrame = CGRectOffset(fooBarViewFrame, 0, FOOBARVIEW_HEIGHT);
    }
}

- (FooBarView*)loadFooBarViewForData:(MBSomeData*)data
{
    NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@"FooBarView" owner:self options:nil];
    FooBarView *fooBarView = [topLevelObjects objectAtIndex:0];
    fooBarView.barView.amountInEuro = data.amountInEuro;
    fooBarView.barView.gradientStartColor = data.color1;
    fooBarView.barView.gradientMidColor = data.color2;
    fooBarView.titleLabel.text = data.name;
    fooBarView.titleLabel.textColor = data.nameColor;
    return fooBarView;
}

Notice, how I set owner of nib to self - there's no harm as I didn't connect "File's owner" to anything. The only valuable result from loading this nib is its first element - my view.

If you want to adapt this approach for creating views in IB, it's pretty easy. Implement loading of subview in a main custom view. Set the subview's frame to main view's bounds, so that they are the same size. The main view will become the container for your real custom view, and also an interface for external code - you open only needed properties of its subviews, the rest is encapsulated. Then you just drop custom view in IB, configure it to be your class, and use it as usual.

Michal
Clearly written, clever, and it works! Thanks Michal.
Mike Morearty
A: 

I have been trying this very thing all day, with only limited success. I started with the Multi View demo which creates views programatically. I'd like to encapsulate views and make them reusable - decoupled freedom - but still allow me to design the interface with IB.

Alas I've been unable to. If you could share a complete skeleton app I would be most appreciative.

ATM, I am able to create 'per page view and controllers' on the fly, ie:

//View01.h
#import <UIKit/UIKit.h>

@interface View01 : UIViewController {
    UIButton *button;
}
@property (nonatomic, retain) IBOutlet UIButton *button;
- (IBAction)changeView:(id)sender;
- (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle;
@end

// View01.m
#import "View01.h"
#import "MultiviewAppDelegate.h"

@implementation View01
@synthesize button;

- (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle {
    self = [super initWithNibName:nibName bundle:nibBundle];
    NSLog(@"View01 - initWithNibName enter");
    if (self) {
        NSLog(@"View01 - initWithNibName self ctor");
    }
    NSLog(@"View01 - initWithNibName leave");
    return self;
}

- (void)viewDidLoad {
    NSLog(@"load01");
    [super viewDidLoad];     
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning]; // Releases the view if it doesn't have a superview
    // Release anything that's not essential, such as cached data
}

- (void)dealloc {
    NSLog(@"dealloc01");

    [super dealloc];
}

// this fails
- (IBAction)changeView:(id)sender
{
    NSLog(@"Change view");
    MultiviewAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
    [appDelegate displayView:1];
}

@end 

But when I place a button, and link up with IB I get the following when I press the control:

2010-10-28 15:28:49.838 Multiview[7795:207] Trying to load ViewView01
2010-10-28 15:28:49.841 Multiview[7795:207] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<MultiviewViewController 0x9333ab0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key button.'

From what I understand, the controller does not recognise the linking because it is the wrong one. Is there some sort of ownership property I have to set to delegate control to to individual UIViewControllers?

Matt Melton