Common Layout Problem with UIView subView Trees

Common Layout Problem with UIView subView Trees

Introduction

A common mistake I've made in my own iOS projects, and seen in other projects out in the wild, is improper layouts of subViews in UIViews.  What happens is that a UIView is created, with a particular frame size.  Then subViews are added, which may contain their own subViews.  But those subView trees are not managing their internal frame references properly, so you'll get really funky positioning problems.

Example

Here is a simple example of what I'm addressing:

The textfield in the red view is using the wrong frame for its own layout.  While the textfield in the green view is doing things "right".

Details

Here's some code that demonstrates what is happening, and why:

- (void)viewDidLoad {

     [super viewDidLoad];

// Do any additional setup after loading the view, typically from a nib.

// Need to get the frame of the window controlling the app

id<UIApplicationDelegate> del = [[UIApplication sharedApplicationdelegate];

UIWindow *window = del.window;

CGRect appFrame = window.frame;

// create a view in which all of the layout will occur

UIView *myView = [[UIView allocinitWithFrame:appFrame];

// give it a background color for easy contrast

myView.backgroundColor = [UIColor blackColor];

// set up positions for the views that will be displaying the correct and

// incorrect ways of managing internal layout.

CGFloat xPosition = 20.0f;

CGFloat wrongYPosition = 100.0f;

CGFloat rightYPosition = 300.0f;

CGFloat width = 300.0f;

CGFloat height = 100.0f;

// craete the frames for the views

CGRect wrongFrame = CGRectMake(xPosition, wrongYPosition, width, height);

CGRect rightFrame = CGRectMake(xPosition, rightYPosition, width, height);

// size of the UITextFields being built for the demo

CGFloat tfWidth = 150.0f;

CGFloat tfHeight = 40.0f;

// frames for the textfields

CGRect tfWrongFrame = CGRectMake(xPosition, wrongYPosition, tfWidth, tfHeight);

CGRect tfRightFrame = CGRectMake(xPosition, rightYPosition, tfWidth, tfHeight);

// build the views with their frames

self.wrongView = [[VEWrongView allocinitWithFrame:wrongFrame];

self.rightView = [[VERightView allocinitWithFrame:rightFrame];

// create the internal textfields with their frames

[self.wrongView layoutTextField:tfWrongFrame];

[self.rightView layoutTextField:tfRightFrame];

// give the views background colors for easy contrast

self.wrongView.backgroundColor = [UIColor redColor];

self.rightView.backgroundColor = [UIColor greenColor];

// add the views to myView

[myView addSubview:self.wrongView];

[myView addSubview:self.rightView];

self.view = myView;

}

In my viewcontroller, I am doing all the heavy lifting for building the initial views in viewDidLoad, including creating their respective frames.  This is where part of the problem lies.  I create wrongFrame and rightFrame, which are the frames for the two main views being displayed.  I then use those same frames as the basis for the textfields being built internally in each subview, by passing the respective frames in through [VEWrongView layoutTextField:] and [VERightView layoutTextField:].

In [... layoutTextField:], I am creating the textfields and adding them to their parent views:

WrongView:

//

//  VEWrongView.m

//  ViewExamples

//

//  Created by Gregory Hill on 7/15/12.

//  Copyright (c) 2012 Hillside Apps, LLC. All rights reserved.

//

 

#import "VEWrongView.h"

 

@implementation VEWrongView

 

@synthesize textField _textField;

 

 

- (id)initWithFrame:(CGRect)frame {

    self = [super initWithFrame:frame];

    if (self) {

        // Initialization code

    }

    return self;

}

 

- (void) layoutTextField:(CGRect)tfFrame {

// Common mistake:  the frame being handed in is the same frame as the 

// this view's.  And then nothing is done to manage the relative nature

// of view layouts.  See [VERightVew layoutTextField:] for an example

// of "doing it right".

UITextField *tf = [[UITextField allocinitWithFrame:tfFrame];

tf.backgroundColor = [UIColor whiteColor];

tf.text = @"This is wrong!";

[self addSubview:tf];

}

 

/*

// Only override drawRect: if you perform custom drawing.

// An empty implementation adversely affects performance during animation.

- (void)drawRect:(CGRect)rect

{

    // Drawing code

}

*/

 

@end

RightView:

//

//  VERightView.m

//  ViewExamples

//

//  Created by Gregory Hill on 7/15/12.

//  Copyright (c) 2012 Hillside Apps, LLC. All rights reserved.

//

 

#import "VERightView.h"

 

@implementation VERightView

 

@synthesize textField _textField;

 

 

- (id)initWithFrame:(CGRect)frame {

    self = [super initWithFrame:frame];

    if (self) {

        // Initialization code

    }

    return self;

}

 

- (void) layoutTextField:(CGRect)tfFrame {

// Though not ideal, this is a better way to handle internal layouts.  Since

// tfFrame is based on the origin of VERightView, we need to reset the

// textField's origin back to zero.

UITextField *tf = [[UITextField allocinitWithFrame:tfFrame];

 

CGRect frame = tf.frame;

frame.origin = CGPointMake(0.0f0.0f);

tf.frame = frame;

 

tf.backgroundColor = [UIColor whiteColor];

tf.text = @"This is right!";

 

[self addSubview:tf];

}

 

 

 

/*

// Only override drawRect: if you perform custom drawing.

// An empty implementation adversely affects performance during animation.

- (void)drawRect:(CGRect)rect

{

    // Drawing code

}

*/

 

@end

The key difference is in how the frames are managed.  In WrongView, I am simply initializing the textfield with tfFrame and then adding it to its parent.  In RightView, however, I am initializing with tfFrame, but then resetting the origin to (0.0, 0.0).  The reasoning for this is simple.  The parent view already has a frame and is being positioned accordingly.  The origin of the textfield is relative to the parent frame.  So whatever value I have for the origin of the textfield will be added to the position of its parent view.

Conclusion

Now, if you understand that, you can obviously use that to your advantage.  But I think a lot of rookie (and not so rookie!) iOS programmers don't have that understanding.  It's a common enough mistake, and I don't really see it being addressed in other blogs or boards.  Just remember, your subviews are always being laid out relative to the position of their parent views.  Simple, obvious, but still problematic if you're not paying attention.

 

TMI Manager

Download our iOS app, TMI Manager 1.0, now!

Comments

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.