tags:

views:

216

answers:

3

A little background:

I'm a C# developer starting to mess with the iPhone (have an idea for a simple 2D game). The only MVC programming I've done was for the web (ASP.NET MVC) so although I do have an understanding in MVC, I can't wrap my mind around one thing. Here's an example to illustrate.

Say I have a simple app where all I want to do is display a big circle on the screen. I created a "View Based Application" and it gave me the basic classes to start with:

MVCConfusionAppDelegate MVCConfusionViewController

Now since I'll be doing some custom drawing (I know I can add a subview and show the circle that way, but this is just a sample of a larger piece) I've added a class called MyCustomView and in Interface Builder set the View of the MVCConfusionViewController to be a MyCustomView.

Now here's the problem. I want to be able to set in code the size of how big the ball on the custom view should be. So I have a property on the MyCusomView like this:

#import <Foundation/Foundation.h>

@interface MyCustomView : UIView {
    NSNumber *ballSize;
}

@property(nonatomic,retain)IBOutlet NSNumber *ballSize;

@end

#import "MyCustomView.h"


@implementation MyCustomView

@synthesize ballSize;

-(void)drawRect:(CGRect)rect
{
    CGContextRef context = UIGraphicsGetCurrentContext();
    [[UIColor redColor]set];

    float floatValue = [self.ballSize floatValue];
    CGRect ballRect = CGRectMake(50.0f, 50.0f,floatValue , floatValue);
    CGContextFillEllipseInRect(context, ballRect);

}

@end

Then, here's my MVCConfusionViewController:

#import <UIKit/UIKit.h>
#import "MyCustomView.h"

@interface MVCConfusionViewController : UIViewController {
    NSNumber *ballSize;
}

@property(nonatomic,retain)IBOutlet NSNumber *ballSize;

@end

#import "MVCConfusionViewController.h"
#import "MyCustomView.h"

@implementation MVCConfusionViewController

@synthesize ballSize;

- (void)viewDidLoad {
    [super viewDidLoad];
    MyCustomView *myView = (MyCustomView *)self.view;
    myView.ballSize = self.ballSize;
}

And finally, the MVCConfusionAppDelegate:

#import <UIKit/UIKit.h>

@class MVCConfusionViewController;

@interface MVCConfusionAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    MVCConfusionViewController *viewController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet MVCConfusionViewController *viewController;

@end

#import "MVCConfusionAppDelegate.h"
#import "MVCConfusionViewController.h"
#import "MyCustomView.h"

@implementation MVCConfusionAppDelegate

@synthesize window;
@synthesize viewController;


- (void)applicationDidFinishLaunching:(UIApplication *)application {    

    viewController.ballSize = [NSNumber numberWithInt:200];
    [window addSubview:viewController.view];

    [window makeKeyAndVisible];
}


- (void)dealloc {
    [viewController release];
    [window release];
    [super dealloc];
}


@end

As you can see, there's an ugly cast in my viewDidLoad method. I was hoping I'd be able to make the connection of the ballSize properties in IB, but it won't let me.

So my question simply is, what's the correct way of passing this data from my view controller to my view without doing that cast? I know I'm missing something fundamental, but I just don't see it. Any help would be greatly appreciated!

EDIT: Here's the source code. http://bit.ly/uKyp9 Maybe someone can have a look and see if I'm doing anything wrong.

+1  A: 

Your view should already be set in IB, so you can use it as is. If you want to use MyCustomView, you can do it like this:

- (void)viewDidLoad {
    [super viewDidLoad];
    CGRect frame = CGRectMake(0, 0, 320, 480);
    MyCustomView *myView = [[MyCustomView alloc] initWithFrame:frame];
    myView.backgroundColor = [UIColor greenColor];
    self.view = myView;
    [myView release];
    CGRect rectangle = CGRectMake(20, 20, 20, 20);
    [self.view drawRect:rectangle];
}

I couldn't make your drawing code work, I don't know much about that.

nevan
I added the source code, see if it works for you now. Also, IB is already adding the MyCustomView for me because I set the view of the ViewController to be of type MyCustomView in IB.
BFree
+1  A: 

One way to avoid the cast would be to add a separate outlet property for the custom view on the controller, and refer to that instead.

In Interface Builder, make an instance of MyCustomView and drag it into the existing view to make it a subview, then attach it to its own outlet on the controller.

Will Harris
+2  A: 

Are you trying to connect one IBOutlet (in the controller) to another IBOutlet (in the view)? Unfortunately, I don't think it's that easy :-)

You're also storing the data (ballSize) in the controller and the view.

I'd make MVCConfusionViewController a data source for MyCustomView, and then let MyCustomView ask its datasource for the ballSize, inside the -drawRect: method.

@class MyCustomView;

@protocol MyCustomViewDataSource
- (NSNumber *)ballSizeForMyCustomView:(MyCustomView *)view;
@end

@interface MyCustomView {
    id<MyCustomViewDataSource> dataSource;
}
@property (nonatomic, assign) IBOutlet id<MyCustomViewDataSource> dataSource;
@end

@implementation MyCustomView
- (void)drawRect:(CGRect) rect {
    if (self.dataSource == nil) {
        // no data source, so we don't know what to draw
        return;
    }

    float floatValue = [[self.dataSource ballSizeForMyCustomView:self] floatValue];
    // ...
}
@end

In Interface Builder, hook MVCConfusionViewController up to the view's dataSource property. Then implement the protocol:

@interface MVCConfusionViewController : UIViewController <MyCustomViewDataSource> {
    [...]
}
[...]
@end

@implementation MVCConfusionViewController
- (NSNumber *)ballSizeForMyCustomView:(MyCustomView *)view {
    return self.ballSize;
}
@end

This way your view controller could also be the data source for multiple MyCustomViews, because the protocol method takes a MyCustomView as an argument.

If you need more than one ball, have a look at the UITableViewDataSource and implement similar methods, something like:

-(NSInteger)numberOfBallsInMyCustomView:(MyCustomView *)view;
-(NSNumber *)myCustomView:(MyCustomView *) ballSizeAtIndex:(NSInteger)index;
Thomas Müller
OK, this makes loads of sense. I'm going to try and implement this now using this approach. If it works out and makes sense, I'll accept your answer. Thanks!
BFree
OK, I'm accepting this as the answer. It seems to be the most logical way of doing it, and I like the protocol approach. Decouples the Controller and the View. I did have to change your code though, the protol method doesn't need the View as an argument. In fact, that causes a circular reference, and won't build. Thanks loads for your help.
BFree
Really? Well, I just typed all of that into the browser. Anyways, I'm glad I could help.
Thomas Müller
I've added a forward declaration for MyCustomView before the @protocol (see my edited answer). That might solve your problem with compiling the program. I tested this with a test project, and that's the only thing I added to the code in my original answer.
Thomas Müller
Still new to Objective-C so I'm not quite sure what a forward declaration is, but more than that though; what's the point of having the View as an argument to the method? It doesn't seem like you're using it? Is that just best practice? What would be the use of it?
BFree
I'm not using it in this example, but it allows your view controller to be the data source for more than one view. Every example of delegates and datasources in Cocoa Touch that I've looked at uses this approach.
Thomas Müller
A forward declaration tells the compiler that a certain class exist, without the need to import the header file. In my small test project for this question the @protocol was in the same file as the view's @interface, but before the @interface. My data source method was referring to the view class, which hadn't been declared yet. HTH
Thomas Müller