The problem: Let's suppose I have an image and an element which constantly triggers viewDidLayoutSubviews (a scrollview.. so that at every scroll viewDidLayout would be called... or whatever element that triggers viewDidLayout quite often).
This image as well as the "viewDidLayout_element" are all set up well with autolayout in the storyboard.
Now:
I need to make the image to be a rounded one.
with the typical: layer.cornerRadius.. and layer.masksToBounds. This requires a calculated imageView frame.
In what view controller cycle do I make it programatically? Taking into consideration that:
*except for the viewDidLayoutSubviews I don't get the right frame
*the viewDidLayoutSubviews can be called even thousands of times depending on the "viewDidLayout_element" that triggers it.
*If I call layoutIfNeeded on the imageView in viewDidAppear (because it's the only case when frames are already available so we can force their calculation) the user will already catch a glimpse of the transformation from a square image to a circular image. In other words in viewDidAppear the frame becomes available for our manipulation, but is also available for milliseconds to the user's eyes.
*it does not make sense to fill the viewDidLayoutSubviews with flags (especially if there will be something more performance intensive than a circular imageView transformation) like below:
if !iDidChangedTheImage
{
imageView.applyCornerRadius()
}
The question:
Where do I have the correct frame size inside a viewController cycle without the problems from above? Or how do you usually solve this problem?
Subclass UIImageView to your custom class, and override layoutSubviews. After that you don't need to care about image view size change and updating corner radius, just use that class for your custom image view
#implementation CustomImageView
- (void)awakeFromNib {
[super awakeFromNib];
self.layer.masksToBounds = YES;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.layer.cornerRadius = self.frame.size.height / 2.0;
}
#end
Related
I'm trying to create a view controller to simulate a classic weighing scale. I have a UIView subclass (DragView) to represent the weights, and a another UIView subclass (ContainerView) to simulate the plates os the scale.
When a DragView is drag over the ContainerView, I trigger an animation to place the DragView inside the ContainerView (changing the size if is necessary). But, if the user releases the DragView outside the ContainerView, then the DragView is animated to its original position and size.
Here you can see the DragView (in green) and two ContainerView (in clear Color above the "plates")
The original frame of the DragView is set with constraints (proportional width, top and leading). Everything looks fine but when I animate the DragView back to his original position, then I've got this.
See the difference in the DragView's frame?. Why is this happening?
Here are the relevant parts of my code.
DragView.m
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
_originalFrame = self.frame;
}
return self;
}
- (void)animateBackToOrigin
{
[UIView animateWithDuration:0.1 animations:^{
self.frame = _originalFrame;
}];
}
I've checked the _originalFrame values in both methods and it returned the same values.
ANSWER:
My mistake was setting the _originalFrame within initWithCoder, layoutSubViews is the right place. Because layoutSubViews is called every time the view is set, I added a check (with CGRectIsEmpty) in order to set the frame only if there is no value.
- (void)layoutSubviews
{
if (CGRectIsEmpty(_originalFrame)) {
_originalFrame = self.frame;
}
}
It is to early in initWithCoder: to take resulting frame. The view is just instantiated and not processed through layout process. I think, the best place is layoutSubviews method.
When autolayout is present bad things will happen if you mess with frame.
Try instead of changing the full frame, change the .origin of the object
I'm developing iOS UI with auto layout.
Because I need to set corner radius of view's layer for make view looks like circle, get actual view's size is necessary. (I couldn't change layer by constraints)
Finally, I realize I can get view's real size in viewDidLayoutSubviews and I looks well. But when I called it's super method, size becomes wrong value.
- (void)viewDidLayoutSubviews {
// [super viewDidLayoutSubviews];
if (_viewProfileCoverForReward) {
float circleSize = imageViewOppositeProfile.frame.size.width;
_viewProfileCoverForReward.layer.cornerRadius = circleSize / 2;
NSLog([NSString stringWithFormat:#"(%ld)circle size : %f", super.missionData.missionID, circleSize]);
}
}
Here is my code and it works well. But in my opinion, don't call super's method is not good and why I must not call super's method if I want to get view's real size. It doesn't make sense!
I am working on my first project using Auto Layout and custom views. My question is this:
I created my custom view in Interface Builder then added constraints to stretch the view if needed which is working the way I want it to, however, consider the following code snippet from my custom view class -
// MyCustomView
-(id)initWithCoder:(NSCoder*)decoder
{
self = [super initWithCoder:decoder];
if(self != nil)
{
CGFloat layerWidth = self.bounds.size.width;
CGFloat layerHeight = self.bounds.size.height;
}
return self;
}
This code return the size set in IB. My drawing code relies on the new width and height (if the view has been stretched) but I don't know how to retrieve them.
First, that code is utterly silly because you are creating variables layerWidth and layerHeight and throwing them away, which is pointless.
Second, self.bounds.size is always the view's width and height. However, it is pointless to ask about this in initWithCoder:, which (as you have rightly seen) happens long before the view is put into the interface and even longer before the auto layout takes place that resizes it. If your drawing code relies on the bounds size, then retrieve the bounds size when you draw. If you need to draw again because the view has changed size, and if this is not happening all by itself, then implement layoutSubviews to tell the view that it needs to be drawn again.
I have a custom UIView fooView, where I've overridden the drawRect method, with an initWithFrame method.
I also have a custom UIView barView which holds and contains fooView.
barView is supposed to get it's height from fooView since fooView is drawing custom stuff.
My problem is everytime I check for fooView's frame, or bounds property, it remains the same. Even though I can clearly see it outgrowing the initial height dictated by the initWithFrame.
Which leads me to believe that maybe since I've overridden the drawRect method, now it's my responsibility to update fooView's frame.
Should I do this?
How would I do this?
What is the best practice?
EDIT: Added Code
- (id)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) [self setupWithMessage:nil];
return self;
}
setupWithMessage just calculates the dimensions (mainly height) required for text to fit in a constrained width.
- (void)drawRect:(CGRect)rect{
[super drawRect:rect];
// a whole bunch of drawing logic, but basically draws a message bubble
// based on the size of the text (The text dynamically changes), and then
// draws the text so if there is a lot of text, the calculated height of
// what it takes to draw the text can easily be larger than the size
// originally passed in during initialization
}
The method drawInRect:(CGRect)rect gives you a rect that is not greater than your's view frame.
So if you draw in rect that is higher that given rect - your drawings will be clipped.
So steps that you should do:
Calculate all dimensions and set your's fooView frame.
After that you should call [fooView setNeedsDisplay].
It will invoke your drawInRect: method with a new frame, so you can focus there on drawing.
If your fooView main task is to draw given text from barView I suggest you to create #property (nonatomic) NSString *textToDraw; in fooView class and override setter in which you will follow steps above.
EDIT:
As I see you created a setupWithText: method for this purposes. So you need to calculate all dimensions, set frame self.frame = (CGRect){previousX, previousY, newWidth, newHeight} and call [self setNeedsDisplay];
I have following setup
XIB file which has only landscape view. This view is connection to my controller
There is a label on this view which is connected to IBOutlet UILabel* label
This label is configured like this (it occupies the whole width of screen).
I overrided viewWillAppear and do this (to get the size of label).
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGRect rect = _labelTitleLand.frame;
}
The strange thing (which I don't understand). That it returns size = (width 768, height 21) when it launched in portrait (on iPad), which is correct.
And it returns size = (width 741 height 21) when it's launched in landscape. Which is weird. I anticipated that it will return width 1024, height 21 for landscape.
I was under impression that at the moment of viewWillAppear, all controls sizes are calculated already.
Update 1
If I check labelTitleLand.frame on viewDidAppear then it returns correct results. However, I don't like this, because I want to do some actions (based on this size) which influence how view will be drawn. In the case, if I will do it on viewDidAppear, as I understand there will be visible redrawing.
The layout of the view hierarchy has to be complete before you will get the actual final frames.
So you should check the frame in viewDidLayoutSubviews, which will still be before the view hierarchy is actually drawn. If you need to make changes here you can without causing any redrawing to occur.
viewWillAppear is too early because this is before your autoresizing masks (and/or autolayout constraints) have had their effect.
This seems a problem related to when a method is actually called at runtime.
I solved similar situation using
[self performSelector:#selector(checkMethod) withObject:nil afterDelay:0.1];
in viewWillAppear, and then:
- (void)checkMethod
{
rect = _labelTitleLand.frame;
}
This gives your app the time needed to set its own frame.
It is not so elegant and it looks like a workaround, but it is very effective.
You can also try to force the frame of the UIView that is container of the UILabel in viewWillAppear like this:
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.view.frame = CGRectMake(0.0f, 0.0f, 1024.0f, 768.0f);
CGRect rect = _labelTitleLand.frame;
}
But the first solution is more reliable and usually no lag is experienced.