Problems creating a custom UIView - iphone

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.

Related

Adding iOS UITableView HeaderView (not section header)

I want to add a table header (not section headers) like in the contacts app for example:
exactly like that - a label beside an image above of the table.
I want the all view be scrollable so I can't place those outside of the table.
How can I do that?
UITableView has a tableHeaderView property. Set that to whatever view you want up there.
Use a new UIView as a container, add a text label and an image view to that new UIView, then set tableHeaderView to the new view.
For example, in a UITableViewController:
-(void)viewDidLoad
{
// ...
UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(XXX, YYY, XXX, YYY)];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(XXX, YYY, XXX, YYY)];
[headerView addSubview:imageView];
UILabel *labelView = [[UILabel alloc] initWithFrame:CGRectMake(XXX, YYY, XXX, YYY)];
[headerView addSubview:labelView];
self.tableView.tableHeaderView = headerView;
[imageView release];
[labelView release];
[headerView release];
// ...
}
You can do it pretty easy in Interface Builder. Just create a view with a table and drop another view onto the table. This will become the table header view. Add your labels and image to that view. See the pic below for the view hierarchy.
In Swift:
override func viewDidLoad() {
super.viewDidLoad()
// We set the table view header.
let cellTableViewHeader = tableView.dequeueReusableCellWithIdentifier(TableViewController.tableViewHeaderCustomCellIdentifier) as! UITableViewCell
cellTableViewHeader.frame = CGRectMake(0, 0, self.tableView.bounds.width, self.heightCache[TableViewController.tableViewHeaderCustomCellIdentifier]!)
self.tableView.tableHeaderView = cellTableViewHeader
// We set the table view footer, just know that it will also remove extra cells from tableview.
let cellTableViewFooter = tableView.dequeueReusableCellWithIdentifier(TableViewController.tableViewFooterCustomCellIdentifier) as! UITableViewCell
cellTableViewFooter.frame = CGRectMake(0, 0, self.tableView.bounds.width, self.heightCache[TableViewController.tableViewFooterCustomCellIdentifier]!)
self.tableView.tableFooterView = cellTableViewFooter
}
You can also simply create ONLY a UIView in Interface builder and drag & drop the ImageView and UILabel (to make it look like your desired header) and then use that.
Once your UIView looks like the way you want it too, you can programmatically initialize it from the XIB and add to your UITableView. In other words, you dont have to design the ENTIRE table in IB. Just the headerView (this way the header view can be reused in other tables as well)
For example I have a custom UIView for one of my table headers. The view is managed by a xib file called "CustomHeaderView" and it is loaded into the table header using the following code in my UITableViewController subclass:
-(UIView *) customHeaderView {
if (!customHeaderView) {
[[NSBundle mainBundle] loadNibNamed:#"CustomHeaderView" owner:self options:nil];
}
return customHeaderView;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Set the CustomerHeaderView as the tables header view
self.tableView.tableHeaderView = self.customHeaderView;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0,0,tableView.frame.size.width,30)];
headerView.backgroundColor=[[UIColor redColor]colorWithAlphaComponent:0.5f];
headerView.layer.borderColor=[UIColor blackColor].CGColor;
headerView.layer.borderWidth=1.0f;
UILabel *headerLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 5,100,20)];
headerLabel.textAlignment = NSTextAlignmentRight;
headerLabel.text = #"LeadCode ";
//headerLabel.textColor=[UIColor whiteColor];
headerLabel.backgroundColor = [UIColor clearColor];
[headerView addSubview:headerLabel];
UILabel *headerLabel1 = [[UILabel alloc] initWithFrame:CGRectMake(60, 0, headerView.frame.size.width-120.0, headerView.frame.size.height)];
headerLabel1.textAlignment = NSTextAlignmentRight;
headerLabel1.text = #"LeadName";
headerLabel.textColor=[UIColor whiteColor];
headerLabel1.backgroundColor = [UIColor clearColor];
[headerView addSubview:headerLabel1];
return headerView;
}

Where can I specify which UIView is used for the view property of a UIViewController?

I want to set the view property of a UIViewController at runtime. I have an .xib file with two views, and I want my UIViewController subclass that owns the .xib file to decide which UIView to use at runtime. I thought I could do this in loadView by just saying
if(some condition)
self.view = thisView;
else
self.view = thatView;
but that didn't work. How can I do this?
If you want to choose your view dynamically, set it inside -[UIViewController loadView]. A word of caution though: calling -[UIViewController view] will call -[UIViewController loadView] if the view hasn't been loaded yet, so if you do this:
-(void)loadView
{
self.view = [[[UIView alloc] initWithFrame:CGRectZero] autorelease];
self.view.backgroundColor = [UIColor redColor];
}
The second line of that method will call -loadView, and you'll get infinite recursion (which will lead to a stack overflow, and a crash). You need to setup your view, then set the .view property when you've set it up, like this:
-(void)loadView
{
UIView *newView = [[[UIView alloc] initWithFrame:CGRectZero] autorelease];
newView.backgroundColor = [UIColor redColor];
self.view = newView;
}
So you'll probably want to do something like this:
-(void)loadView
{
UIView *newView = nil;
if (self.theSkyIsBlue) {
newView = [[[BlueSkyView alloc] initWithFrame:CGRectZero] autorelease];
newView.backgroundColor = [UIColor blueColor];
}
else {
newView = [[[GraySkyView alloc] initWithFrame:CGRectZero] autorelease];
newView.backgroundColor = [UIColor grayColor];
}
self.view = newView;
}
Addendum 1 - update to show how to use a container view for different views defined in a XIB
If you want to reference other stuff in your XIB, a better approach is to use your .view as a "container view" for your other views. Set it up in -viewDidLoad, like this:
- (void)viewDidLoad
{
UIView *childView = nil;
if (someCondition) {
childView = self.blueView;
}
else {
childView = self.grayView;
}
[self.view addSubview:childView];
childView.frame = self.view.bounds;
}
Note that if you want to swap your views later on, you should make childView a property, instead of a local variable, so you can remove the old childView when inserting a new one.
Inside -(void)loadView; method is where you create your view, so there is where you want to set it conditionally ;)

Background image for MoreNavigationController

I'd like to place an image behind the tableView in my UITabBarController moreNavigationController. I have tried inserting a subview like so when first setting up the TabBar:
UIImageView* imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"background3.png"]];
[self.tabBarController.moreNavigationController.topViewController.view insertSubview:imageView atIndex:0];
But this places the image over the top, presumably because the tableView isn't there at the time. Is there a better time when I can call this in order to have it work properly, or an easier approach?
With some assistance from this question, I figured out how to do this. Basically, the viewController in the moreNavigationController is a single TableView, so adding a background image won't work. What I need to do was to create a new view, add the background image, and then add the moreNavigationController view on top of that. I did this by overriding viewDidLoad in a subclass of UITabBarController, but I expect it could be done elsewhere as well.
- (void)viewDidLoad {
[super viewDidLoad];
UINavigationController *moreController = self.moreNavigationController;
if ([moreController.topViewController.view isKindOfClass:[UITableView class]]) {
UIView* newView = [[UIView alloc] initWithFrame:CGRectMake(0,0,320,367)];
UIImageView* imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"background3.png"]];
imageView.opaque = NO;
imageView.alpha = 0.4;
[newView addSubview:imageView];
moreController.topViewController.view.backgroundColor = [UIColor clearColor];
moreController.topViewController.view.frame = CGRectMake(0,0,320,367);
[newView addSubview:moreController.topViewController.view];
moreController.topViewController.view = newView;
}
}
You could probably be smarter with the frame sizes, etc, but this works for me. Hopefully it helps someone else too.
Now you can acess backgroundView property from UITableView subclasses .
UIViewController *moreViewController = tabBarController.moreNavigationController.topViewController;
img = [[UIImageView alloc]initWithImage:[UIImage imageNamed:#"BG_MORE+1.png"]];
//Got some crashs in initialization !! Need to check .
if ([moreViewController.view isKindOfClass:[UITableView class]]) {
UITableView *moreTableView = (UITableView*)moreViewController.view;
[moreTableView setBackgroundView:img];
}
Besides all the dotty mess here, you can use UIView's bringSubviewToFront: and sendSubviewToBack: to organize your subviews. Basically this should help, although if you have more subviews you will need to play around with it a little bit:
[self.tabBarController.moreNavigationController.topViewController.view addSubview:imageView];
[self.tabBarController.moreNavigationController.topViewController.view pushSubviewToBack:imageView];
//or [self.tabBarController.moreNavigationController.topViewController.view bringSubviewToFront:tableView];

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

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

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.