UIView: layoutSubviews vs initWithFrame - ios

When subclassing UIView, I usually place all my initialisation and layout code in its init method. But I'm told that the layout code should be done by overriding layoutSuviews. There's a post on SO that explains when each method gets called, but I'd like to know how to use them in practice.
I currently put all my code in the init method, like this:
MyLongView.m
- (id)initWithHorizontalPlates:(int)theNumberOfPlates
{
self = [super initWithFrame:CGRectMake(0, 0, 768, 1024)];
if (self) {
// Initialization code
_numberOfPlates = theNumberOfPlates;
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:self.frame];
[scrollView setContentSize:CGSizeMake(self.bounds.size.width* _numberOfPlates, self.bounds.size.height)];
[self addSubview:scrollView];
for(int i = 0; i < _numberOfPlates; i++){
UIImage *img = [UIImage imageNamed:[NSString stringWithFormat:#"a1greatnorth_normal_%d.jpg", i+1]];
UIImageView *plateImage = [[UIImageView alloc] initWithImage:img];
[scrollView addSubview:plateImage];
plateImage.center = CGPointMake((plateImage.bounds.size.width/2) + plateImage.bounds.size.width*i, plateImage.bounds.size.height/2);
}
}
return self;
}
It's the usual tasks: setting up the view's frame, initialising an ivar, setting up a scrollview, initialising UIImages, placing them in UIImageViews, laying them out.
My question is: which of these should be done in init, and which of these should be done in layoutSubviews?

Your init should create all the objects, with the required data. Any frame you pass to them in init should ideally be their starting positions.
Then, within layoutSubviews:, you change the frames of all your elements to place them where they should go. No alloc'ing or init'ing should take place in layoutSubviews:, only the changing of their positions, sizes etc...

In case you're autoresizing works perfectly with just autoresizingFlags, or autolayout, you may just use init to setup the whole view.
But in general your should do layouting in layoutSubviews, since this will be called on every change of the views frame and in other situation, where layout is needed again. Sometimes you just don't know the final frame of a view within init, so you need to be flexible as mentioned, or use layoutSubviews, since you do the layout there after the final size has been set.
As mentioned by WDUK, all initialization code / object creation should be in your init method or anywhere, but not in layoutSubviews.

Related

Setting up a UIScrollView. UIImageView images are distorted if called in viewDidLayoutSubviews. Image included

I am having a strange problem here. I have a UIScrollView that is setup in the interface builder. I add content to it via a method called setupScrollView which is as follows :
-(void)setupScrollView {
UIView *content = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.scrollView.frame.size.width*[self.stickers count], self.scrollView.frame.size.height)];
for(int i=0; i<[self.stickers count]; i++){
UIImageView *imageView= [[UIImageView alloc] initWithFrame:CGRectMake(i*self.scrollView.frame.size.width, 0, self.scrollView.frame.size.width, self.scrollView.frame.size.height)];
MySticker *sticker = self.stickers[i];
imageView.image=sticker.image;
[content addSubview:imageView];
NSLog(#"scroll view width is %f",self.scrollView.frame.size.width);
NSLog(#"ImageView width is %f", imageView.frame.size.width);
}
self.scrollView.contentSize=CGSizeMake(content.frame.size.width ,content.frame.size.height);
[self.scrollView addSubview:content];
}
Now I call this method in viewDidLayoutSubviews because I need the views to be repositioned first after using the constraints set in the interface builder. This all works fine but my images in the scroll view are slightly distorted with jagged edges, there are sticker/chat images with black outlines so it's quite noticeable. If however I place the [self setupScrollView] in the viewDidLoad method the images won't be distorted and look perfectly clear. Example image:
Have no idea why it is doing this. Could anyone give me some pointers to what I might be doing wrong?
If it works correctly in viewDidLoad try to call setupScrollView method in viewWillLayoutSubviews method.
Also check situation when setupScrollView will be call more than once to avoid adding duplicates.

should adding a UILabel in a custom UIView be in drawRect or initWithFrame / custom setup method

I'm not a full-time iOS dev and have to make some changes to someone else's code. We have a custom view where a UILabel is added in drawRect like this (edited for brevity):
- (void)drawRect:(CGRect)rect
{
UILabel *myLabel=[[UILabel alloc] initWithFrame:CGRectMake(50.0f, 50.0f, 100.0f, 30.0f)];
myLabel.text=#"here is some text";
[self addSubview:myLabel];
}
I have never really seen this and thought that drawRect was ONLY for adding drawing operations (and have only seen UIBezierPaths). Should this be moved to initWithFrame (or a common setup method like setupMyView). Or is ok to leave in drawRect? Is there anything besides custom drawing that should be in drawRect?
Sorry for asking a somewhat basic question but even reading the Apple docs leave a bit to be desired.
Unless there is a very good reason to setup the view's subviews from within the drawRect method (I can think of none) I would strongly suggest leaving drawRect as purely a drawing method and move that addSubview stuff out of there! I would suggest overriding the -init method that is currently used to initiate the parent view. For example:
-(instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:(CGRect)frame]) {
UILabel *myLabel=[[UILabel alloc] initWithFrame:CGRectMake(50.0f, 50.0f, 100.0f, 30.0f)];
myLabel.text=#"here is some text";
[self addSubview:myLabel];
}
return self;
}
I would definitely not do anything like that in drawRect. One method I've used in the past is to add it to layoutSubviews, because at that point the view is aware of the true bounds/frame. You'll want to ensure that you only generate the view stuff once, as layoutSubviews is called many times, such as on rotation. I usually do something like the following, where _viewGenerated is an instance variable:
- (void)layoutSubviews {
[super layoutSubviews];
if (!_viewGenerated) {
[self generateView];
_viewGenerated = YES;
}
}
- (void)generateView {
// do everything with any labels, images, etc., here...
}

CGRectIntersectsRect with 2 UIImageViews does not work

I have a problem.
Briefly: CGRectIntersectsRect(object1.frame, object2.frame) placed in UIViewController does not work for object1 and object2 created in different classes (Class1 and Class2). I only can change their coordinates like object1.center.x, and object1.frame.size.width = 0 (I guess this is why CGRectIntersection function does not work). These objects just came through each other.
As I understand it may be connected with protocol/delegating, but I haven't found any sufficient information how to be in my situation.
Platform.m (my class for creating a platform in the ViewController)
#implementation Platform
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
platformView = [[UIImageView alloc]initWithFrame:frame];
platformView.image = [UIImage imageNamed:#"platform.png"];
[self addSubview:platformView];
}
return self;
}
ViewController.m (in the #interface there are Platform *platform; Kelvin *kelvin;)
-(void)addPlatform
{
platform = [[Platform alloc]initWithFrame:CGRectMake(initialPlatformX, (500 - arc4random()%200), 200, 10)];
[self.view addSubview:platform];
}
**if (!CGRectIntersectsRect(kelvin.frame, platform.frame)) {
NSLog(#"%f", platform.frame.size.width);
}**
Your explanation is confusing and hard to follow.
This sentence, for example, is a total mystery to me:
As I understand it may be connected with protocol/delegating, but i
haven't found any sufficient information how to be in my situation.
Delegation? Huh? Rectangles are not objects. Delegation is a design pattern for objects. How is that relevant here? (Answer: Very likely it isn't relevant at all.)
Moving on, though, here are a few things to consider:
2 views' frames will only use the same coordinate system if they are both subviews of the same view. So are your platform and your "kelvin" view subviews of the same view (It looks like you add platform directly to the view controller's content view, so is your "kelvin" view also a subview of the view controller's content view?)
Also, you make reference to object1.frame.size.width = 0
The CGRectIntersectsRect function checks to see if any pixels of the 2 rectangles overlap. If either of your rectangles has either a height or a width of 0, it's empty, and can't intersect with anything.
In initWithFrame, you are adding a UIImageView subview to Platform using the frame of Platform. That is incorrect. For example, if you add PlatForm at CGRectMake(10, 10, 100, 100), you'll then be adding the image view at CGRectMake(10, 10, 100, 100) within Platform (which is CGRectMake(20, 20, 100, 100) within the original view).
So, instead, I think you want to make a CGRect that starts at 0, 0 (or use the bounds of Platform, not its frame):
#implementation Platform
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
platformView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)];
platformView.image = [UIImage imageNamed:#"platform.png"];
[self addSubview:platformView];
}
return self;
}
By the way, just to be safe, you might also want to set clipsToBounds to YES, so that if you ever change contentMode to something that doesn't scale to fit, you don't have to worry about the image bleeding over its bounds.

Is UICollectionView.backgroundView broken

I'm writing something relatively simple, or so I thought.
Firstly, the code, for which I'm trying to place an image on the background of the UICollectionView if there are no results returned from my server. The image is 200 by 200:
UIView *myView = [[UIView alloc] initWithFrame:self.view.bounds];
CGRect myViewSpace = self.view.bounds;
CGFloat myX = (myViewSpace.size.width /2.0) - 100;
CGFloat myY = (myViewSpace.size.height /2.0) - 100;
UIImageView *imView = [[UIImageView alloc] initWithFrame:CGRectMake(myX, myY, 200, 200)];
imView.image = [UIImage imageNamed:#"imNotHome"];
[myView addSubview:imView];
myCollectionView.backgroundView = myView;
Once there are results, I want to be able to remove it.
I thought it'd be as simple as placing the following, before I reloaded the UICollectionView:
[myCollectionView.backgroundView removeFromSuperview];
However, it appears to be doing nothing.
Am I missing something?
Thanks in advance!
It should be done this way instead:
myCollectionView.backgroundView = nil;
Explanation: You should unset the UICollectionView's background in the same way as you set it. You didn't set it by manipulating the view hierarchy, but by setting the background property. You did call addSubview in the previous line, but that was to add a subview to your background view, not to add the background view itself.
Edit:
There is a very good article about this UICollectionView bug here:
http://blog.spacemanlabs.com/2013/11/uicollectionviews-backgroundview-property-is-horribly-broken/
The solution the author gives is to reset the background to an empty opaque view:
UIView *blankView = [UIView new];
blankView.backgroundColor = [UIColor whiteColor];
[myCollectionView.backgroundView removeFromSuperview];
myCollectionView.backgroundView = blankView;
Also, the author recommends not using the backgroundView property at all but doing it yourself:
Frankly, I think the best solution is to just ignore the backgroundView property all together. Instead, make the collection view’s background clear, and implement your own backgroundView; just throw a view behind the collection view.

Add overlay image on top of drawRect

I have a UIView subclass and I am drawing in the drawRect method.
How can I add an overlay image on top of the drawRect target layer?
I add the following in the initWithFrame method:
//add the image overlay
UIImageView *overlageImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"downloaderFront.png"]];
overlageImage.center = self.center;
[self.layer addSublayer:overlageImage.layer];
//replacing the above line with below line does not fix either
[self addSubview:overlageImage];
This image is not visible on top of the drawRect drawing.
The stuff with the layer is certainly wrong. Just do this:
UIImageView *overlageImage =
[[UIImageView alloc] initWithImage:[UIImage imageNamed:#"downloaderFront.png"]];
[self addSubview:overlageImage];
You can play with the frame of overlageImage to get the position where you want it.
As to your actual question: put a breakpoint. I suspect you'll find that your code is never even running (because your view's initWithFrame is never called). That's just a guess - you didn't say anything about how this UIView subclass instance gets into the interface - but it's a common enough mistake, and one should always suspect the obvious first!

Resources