Need to override drawrect if your UIView is merely a container? - iphone

According to Apple's docs, "Subclasses need not override -[UIView drawRect:] if the subclass is a container for other views."
I have a custom UIView subclass that is indeed merely a container for other views. Yet the contained views aren't getting drawn. Here's the pertinent code that sets up the custom UIView subclass:
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]))
{
// Consists of both an "on" light and an "off" light. We flick between the two depending upon our state.
self.onLight = [[[LoyaltyCardNumberView alloc] initWithFrame:frame] autorelease];
self.onLight.backgroundColor = [UIColor clearColor];
self.onLight.on = YES;
[self addSubview:self.onLight];
self.offLight = [[[LoyaltyCardNumberView alloc] initWithFrame:frame] autorelease];
self.offLight.backgroundColor = [UIColor clearColor];
self.offLight.on = NO;
[self addSubview:self.offLight];
self.on = NO;
}
return self;
}
When I run the code that displays this custom UIView, nothing shows up. But when I add a drawRect method...
- (void)drawRect:(CGRect)rect
{
[self.onLight drawRect:rect];
[self.offLight drawRect:rect];
}
...the subviews display. (Clearly, this isn't the right way to be doing this, not only because it's contrary to what the docs say, but because it -always- displays both subviews, completely ignoring some other code in my UIView that sets the hidden property of one of the views, it ignores the z-ordering, etc.)
Anyway, the main question: why don't my subviews display when I'm not overriding drawRect:?
Thanks!
UPDATE:
Just to make sure that the problem doesn't lie in my custom subviews, I added in a UILabel as well. So the code reads:
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]))
{
// Consists of both an "on" light and an "off" light. We flick between the two depending upon our state.
self.onLight = [[[LoyaltyCardNumberView alloc] initWithFrame:frame] autorelease];
self.onLight.backgroundColor = [UIColor clearColor];
self.onLight.on = YES;
[self addSubview:self.onLight];
self.offLight = [[[LoyaltyCardNumberView alloc] initWithFrame:frame] autorelease];
self.offLight.backgroundColor = [UIColor clearColor];
self.offLight.on = NO;
[self addSubview:self.offLight];
self.on = NO;
UILabel* xLabel = [[[UILabel alloc] initWithFrame:frame] autorelease];
xLabel.text = #"X";
[self addSubview:xLabel];
}
return self;
The "X" doesn't display either.
UPDATE 2:
Here's the code that invokes my custom UIView (OffOnLightView):
// Container for all of the OffOnLightViews...
self.stampSuperView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)] autorelease];
[self.view addSubview:self.stampSuperView];
// Draw the stamps into the 'stamp superview'.
NSInteger numberOfCardSpaces = (awardType == None) ? 3 : 10;
for (NSInteger i = 1; i <= numberOfCardSpaces; i++)
{
OffOnLightView* newNumberView = [[[OffOnLightView alloc] initWithFrame:[self frameForStampWithOrdinal:i awardType:awardType]] autorelease];
newNumberView.on = (i <= self.place.checkInCount.intValue);
newNumberView.number = [NSString stringWithFormat:#"%d", i];
[self.stampSuperView addSubview:newNumberView];
}

Your subviews should have their frame initialized to the bounds of the parent uiview. Subviews are in a different coordinate system that is relative to the frame of the parent.
self.onLight = [[[LoyaltyCardNumberView alloc] initWithFrame:self.bounds] autorelease];

You should never call -drawRect: manually. If you need to force a redraw, call -setNeedsDisplay.
I would start by debugging (breakpoint or NSLog()) in the -drawRect: methods of the two subviews you are adding to make sure they are actually performing their drawing.
Also note how you're making both subviews the full size (frame) of the containing view, and setting their background colours to clear. I'm going to guess this is intentional, but it's possible they are displaying, but you just can't see anything due to them having a transparent background.

Related

touchevents not triggering from inside subview

I'm having my share of trouble trying to get a touch event through a UIView to it's attached childviews.
I have a number of images I want to do various things with, so I'm interested in reading the touch events on them. It worked fine until I decided to group them in a UIView, so I'm guessing I need to switch some setting on or off to let the events through.
[TouchImageView.h] -------------------------------------------------------
#interface TouchImageView : UIImageView
[TouchImageView.m] -------------------------------------------------------
- (id)initWithFrame:(CGRect)aRect {
if (self = [super initWithFrame:aRect]) {
// We set it here directly for convenience
// As by default for a UIImageView it is set to NO
self.userInteractionEnabled = YES;
state = 0 ;
rect = aRect ;
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// Do what you want here
NSLog(#"touchesBegan!" );
}
[in my delegate's didFinishLaunchingWithOptions] -------------------------
NSArray *myImages = [NSArray arrayWithObjects:
[UIImage imageNamed:#"front-photos.jpg"],
[UIImage imageNamed:#"front-films.jpg"],
[UIImage imageNamed:#"front-presentations.jpg"],
mainMenuView = [[UIView alloc] init ] ;
mainMenuView.userInteractionEnabled = YES; // tried setting this to both YES and NO. Same result.
[self.viewController.view addSubview:mainMenuView] ;
myImage = [[TouchImageView alloc] initWithFrame:CGRectMake(0.0f, menuTopHeight, menuButtonWidth, menuButtonHeight)];
[myImage setImage:[myImages objectAtIndex:0]] ;
[mainMenuView addSubview:myImage] ;
Can someone tell me what I'm doing wrong? I've seen a lot of similar posts here on SO on this topic, but most of them about setting userInteractionEnabled(?)
It turns out that I needed to define a frame for the UIView. So I changed
mainMenuView = [[UIView alloc] init] ;
into this:
mainMenuView = [[UIView alloc] initWithFrame:[UIScreen mainScreen] bounds] ;
and it worked again.
First of all mainMenuView.userInteractionEnabled = NO; should be YES
Also UIImageView has userInteractionEnabled set to NO by deafult, so you will have to set it to YES
After this line
[mainMenuView addSubview:myImage] ;
//add
myImage.userInteractionEnabled = YES;

Put small images on the top of UIImageView?

I've got a UIImageView in a page that gets its image from the Interface builder and I'm trying to put small icons on the top of it (it's a small map and I want to put icons on it). I know it's probably very simple but I tried to look it up in the documentation but it pretty much got me more confused.
Using Interface Builder, can't you just drag a UIImageView into an existing UIImageView? In effect, you end up with one UIImageView embedded within another.
You should also be able to easily set the hidden property of the "small map" UIImageView in code, depending on if that UIImageView is needed or not.
Hope this helps. Good Luck.
Let It Be Known
you could compose your own UIView by adding both the large and small UIViewImage views.
I have illustrated my approach below with the Pseudocode .
-(id) initwithFrame:(CGRect) frame
{
if(self = [super initWithFrame:frame])
{
iContainer = [[UIView alloc] initWithFrame:frame];
[iContainer addSubViews:iLargerUIImageView];
[iContainer addSubViews:iSmallUIImageView];
[self.view addSubViews:iContainer];
}
return self;
}
-(void) layoutSubviews
{
CGRect myRect = self.frame;
iContainer.frame = myRect;
//Give the location to iLargerUIImageView as per your requirement.
iLargerUIImageView.frame = CGRectMake(...,...,...,...);
//Give the location to iSmallUIImageViewas per your requirement.
iSmallUIImageView.frame = CGRectMake(...,...,...,...);
}
-(void) dealloc
{
[iContainer release];
[iLargerUIImageView release];
[iSmallUIImageView release];
}
try this code:
UIImageView *backgroundImageView = (UIImageView *)[self viewWithTag:kBckGndImag];
if(!backgroundImageView){
UIImage *imageName = [UIImage imageNamed:kpointsChartBig];
backgroundImageView = [[UIImageView alloc] initWithFrame:CGRectMake(15, 15, imageName.size.width, imageName.size.height)];
backgroundImageView.image=imageName;
[backgroundImageView setTag:kBckGndImag];
[pointImageView addSubview:backgroundImageView];
[backgroundImageView release];
}
UIImageView *foregroundImageView = (UIImageView *)[self viewWithTag:kForGndImage];
if(!foregroundImageView){
foregroundImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:kpointsColoredChartBig]];
foregroundImageView.contentMode = UIViewContentModeLeft;
foregroundImageView.clipsToBounds = YES;
[pointImageView addSubview:foregroundImageView];
[foregroundImageView release];
}

Changing bounds of imageView of UITableViewCell

I'm trying to place various size images inside imageView of UITableViewCell. I get the image data asynch'ly, create the image, set the content mode of imageView and finally set bounds of imageView. But the code seems insensitive to any changes I made. I want the images to be centered in a 75x75 area. I wrote the below code for this purpose
UIImage* image = [UIImage imageWithData:data];
[holder.imageView setContentMode:UIViewContentModeCenter || UIViewContentModeRedraw];
[holder.imageView setImage:image];
[holder.imageView setBounds:CGRectMake(0,0,75,75)];
[holder.imageView setFrame:CGRectMake(0,0,75,75)];
[holder setNeedsLayout];
Where holder is the UITableViewCell. The result I get is always the same. All images have 75px height and different widths. Can someone help me solve this problem?
I have realized that setting contentMode and bounds properties does not have any effect in that code. I have added an NSLog after the last line and got the results as below:
NSLog(#"imageview:%# bounds and contentMode:%# %#",[holder imageView],[holder.imageView bounds],[holder.imageView contentMode]);
imageview:<UIImageView: 0x39ab8a0;
frame = (0 0; 75 75); opaque = NO;
userInteractionEnabled = NO; layer =
<CALayer: 0x39a92b0>> bounds and
contentMode:(null) (null)
Still no solution
Done, I finally found the solution, it cost me 3 hours though =)
The solution is to change properties like bound,frame,contentMode in -(void)layoutSubviews method of the custom UITableViewCell class. The "trick" is to write layout code in this method, otherwise the code does not have any effect.
Below code did the work for me. It makes rows of the table vertically aligned.
- (void)layoutSubviews {
[super layoutSubviews];
self.imageView.bounds = CGRectMake(0,0,75,75);
self.imageView.frame = CGRectMake(0,0,75,75);
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
CGRect tmpFrame = self.textLabel.frame;
tmpFrame.origin.x = 77;
self.textLabel.frame = tmpFrame;
tmpFrame = self.detailTextLabel.frame;
tmpFrame.origin.x = 77;
self.detailTextLabel.frame = tmpFrame;
}
So the problem with UITableViewCell's is that you have no control over the size of the built-in objects (namely imageView, contentView, accessoryView, backgroundView). When the table changes, your customizations get trampled over.
You can, as Behlul pointed out, force the sizes to be correct by using layoutSubviews, but the problem with that is that layoutSubviews is called every time the table scrolls. That is a lot of unnecessary re-layout calls.
An alternate, method is to add all of your content to the contentView. Similarly if you are customizing the background, you can create a transparent backgroundView and add your custom background view (eg myBackgroundView) as a subview of backgroundView.
This way you can place and size your items how you want them.
The down side is the stock messages are no longer received from the accessory or image views. You just have to create you own.
Hope that helps!
// This code is not tested
// MyCustomTableViewCell
- (id) init{
self = [super initWithStyle: UITableViewCellStyleDefault reuseIdentifier:#"MyReuseIdentifier"];
if(self){
//image view
my_image_view = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"default_image.png"]] retain];
[my_image_view setFrame:CGRectMake(10,10,30,30)];
[self.contentView addSubview:my_image_view];
//labels
my_text_label = [[[UILabel alloc] initWithFrame:CGRectMake(50,10,100,15)] retain];
[self.contentView addSubview:my_text_label];
//set font, etc
//detail label
my_detail_label = [[[UILabel alloc] initWithFrame:CGRectMake(50,25,100,15)] retain];
[self.contentView addSubview:my_detail_label];
//set font, etc
//accessory view
//Whatever you want to do here
//attach "accessoryButtonTapped" selector to button action
//background view
UIView* background_view = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 50)] autorelease];
[background_view setBackgroundColor:[UIColor greenColor]];
background_view.layer.cornerRadius = 17;
background_view.layer.borderWidth = 3;
background_view.layer.borderColor = [UIColor whiteColor].CGColor;
[self setBackgroundView:[[[UIView alloc] init] autorelease]];
[self.backgroundView addSubview:background_view];
}
return self;
}
- (void) setLabelText: (NSString*) label_text{
[my_text_label setText:label_text];
}
- (void) setDetailText: (NSString*) detail_text{
[my_detail_label setText: detail_text];
}
- (void) accessoryButtonTapped{
//call table view delegate's accessoryButtonTappedForRowWithIndexPath method
}
"UIViewContentModeCenter || UIViewContentModeRedraw" is equivalent to 1. It's also not a bitfield. You want UIViewContentModeCenter.
UITableViewCell.imageView is managed by the cell. If you want custom layout, try adding a view to contentView (I'm guessing what you mean by "centered in a 75x75 area"):
UIImageView * iv = [[[UIImageView alloc] initWithImage:image] autorelease];
iv.frame = (CGRect){{0,0},{75,75}};
iv.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin| UIViewAutoresizingFlexibleRightMargin;
iv.contentMode = UIViewContentModeScaleAspectFit;
[holder.contentView addSubview:iv];
try changing the "contentMode" property of imageView to 'UIViewContentModeScaleAspectFit' or 'UIViewContentModeScaleAspectFill'
Create subclass of UITableViewCell:
#interface UITableViewCellSubClass : UITableViewCell
#end
#implementation UITableViewCellSubClass
- (void)layoutSubviews {
[super layoutSubviews];
self.imageView.frame = CGRectMake(0,4,32,32);
self.textLabel.frame = CGRectMake(42,4,300,32);
}
#end

Problems creating a custom UIView

I'm trying to create my own UIView subclass. I've placed it on my view in Interface Builder by dragging out a UIView, then specifying my subclass's name in the Class Identity field. Yet my UIView does not draw itself.
Here--in a simplified example--is the code in my UIView subclass:
- (id)initWithCoder:(NSCoder *)aDecoder
{
if (self = [super initWithCoder:aDecoder])
{
UILabel* label = [[[UILabel alloc] initWithFrame:self.frame] autorelease];
label.text = #"Hello.";
label.textColor = [UIColor whiteColor];
[self addSubview:label];
}
return self;
}
I've seen reference to overriding drawRect:, but honestly I have no idea what I'd do in that method. I'm sure I'm doing something obviously wrong, but I have no idea what.
Any suggestions would be greatly appreciated!
Thanks.
Try using self.bounds instead of self.frame:
UILabel* label = [[[UILabel alloc] initWithFrame:self.bounds] autorelease];
Your view's frame is probably not at origin {0 0}, which means that the label would end up outside your view's visible area.

Does adding many many subviews to a viewcontrollers default view make my app slow?

I have an app where I create many uiviews and add them to the self.view of the UIViewController. My app is running really slowly. I am releasing all of my objects and have no memory leaks (I ran the performance tool). Can anyone tell me what could be making my app so slow? (code is below)
[EDIT] The array has around 30 items. [/EndEdit]
Thanks so much!
Here is the code for the loadView method of my UIViewController:
- (void)loadView {
UIView *contentView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
contentView.backgroundColor = [UIColor whiteColor];
self.view = contentView;
[contentView release];
int length = 0;
for(NSString *item in arrayTips)
{
length++;
[item release];
}
int index = 0;
for(NSString *item in arrayTitles)
{
SingleFlipView *backView = [[SingleFlipView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
backView.userInteractionEnabled = YES;
backView.backgroundColor = [UIColor whiteColor];
[backView setViewIndex:index];
[backView setLastViewIndex:length];
CGRect labelFrame = CGRectMake(10.0f, 0.0f, 300.0f, 30.0f);
UILabel *backLabel = [[UILabel alloc] initWithFrame:labelFrame];
backLabel.textAlignment = UITextAlignmentCenter;
backLabel.userInteractionEnabled = YES;
backLabel.text = item;
backLabel.font = [UIFont fontWithName:#"Georgia" size:24.0f];
backLabel.textColor = [UIColor blackColor];
backLabel.backgroundColor = [UIColor whiteColor];
CGRect textFrame = CGRectMake(10.0f, 30.0f, 300.0f, 110.0f);
UITextView *tbxView = [[UITextView alloc] initWithFrame:textFrame];
tbxView.textAlignment = UITextAlignmentCenter;
tbxView.userInteractionEnabled = YES;
tbxView.editable = FALSE;
tbxView.text = [arrayTips objectAtIndex:index];
tbxView.font = [UIFont fontWithName:#"Arial" size:14.0f];
tbxView.textColor = [UIColor blackColor];
tbxView.backgroundColor = [UIColor whiteColor];
//CGRect labelFrame = CGRectMake(10.0f, 0.0f, 84.0f, 30.0f);
UIImage *nextTip = [[UIImage imageNamed:#"NextTip.png"] retain];
UIImageView *nextTipView = [ [ UIImageView alloc ] initWithImage:nextTip];
nextTipView.frame = CGRectMake(230.0f, -10.0f, 84.0f, 30.0f);
nextTipView.userInteractionEnabled = YES;
UIImageView *view = [[ UIImageView alloc ] init];
view.userInteractionEnabled = YES;
if(self.sexString == #"Men")
{
UIImage *imgTip = [[UIImage imageNamed:#"feet_small.jpg"] retain];
view.image = imgTip;
view.frame = CGRectMake(0.0f, 110.0f, 416.0f, 228.0f); //59*161
[imgTip release];
}
[backView addSubview:view];
[backView addSubview:tbxView];
[backView addSubview:backLabel];
//[backView addSubview:nextTipView];
[self.view addSubview:backView];
[backView release];
[backLabel release];
[nextTip release];
[nextTipView release];
[tbxView release];
[view release];
index++;
[item release];
}
}
It's going to depend upon how many items are in arrayTitles. If you're just adding one or two of these, you shouldn't see a HUGE slowdown; more, and you will. You should probably take a look at the way UITableView handles its cells; only create these as they're actually needed/used, or, better yet, only create one of these, and set its contents on-the-fly.
A few other notes:
== is not a valid string comparison operator in Objective-C; use [string1 isEqualTo: string2]
It appears you're trying to place a lot of these on screen at the same time, which doesn't seem like it would make a lot of sense.
it looks like you've got a spurious [item release] at the end there (you're never retaining item, so there's no need to release it.
the whole first loop ( for(NSString *item in arrayTips)... frightens and confuses me; items in NSArrays are already retained by the array. You shouldn't have to explicitly retain/release them in this way.
Having deep view hierarchies can lead to slow downs that you can often fix through flattening them some with custom views, but if you are using simple views you can have dozens on the screen with no perceptible performance impact, so in general I recommend ignoring how many views you have when you are developing, and then reducing the view count if it proves to be a performance problem.
Having said that, you appear to be setting up something with an unboundedily large number of views which is not good. Without knowing how many entries there are in array titles I can't tell you what is going on exactly, but I suspect that while the actual visual heiarchy with each backView you are creating is fine, making a backView for each item in the array and using indices to have the front most one hide all the other ones behind it is causing you to have way too many views.
So, how to test it:
Add a break to the bottom of your for loop. Make the loop drop out after a single iteration and see if performance improves. If it does, then the huge view hierarchies are your issue. YOu may have to hack up the routine that changes the indexes to make sure it never swaps to an invalid index to test.
If that is the case you have a few options. You could implement a custom view and flatten every backview into a single view, but depending on how many you have that mat not be sufficient, and it is more work than simply building the back views the way you currently are, but on demand instead of at load time:
1) Refactor the code in your for loop into a separate method that makes a backView for a specific title and attaches it to the view at that time.
2) Where ever you are currently altering the indexes to make the backview visible, instead call the new method to actually build and attach the backview at that time
Don't forget to make as many of your views opaque as you can. Transparent views are a major source of performance issues.