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.
Related
Im learning iOS development right now, XCode doesnt allow me to edit width and height of buttons which are in stack view:
In the Storyboard I create a new button of size 30 x 30 with a custom image and then make more 5 copies of that button. Then I embed them after selecting all of them in a Stack View. Now a disaster happens, the buttons are resized to god knows what size and they appear huge and when I try to go to size inspector to resize those buttons I see that "Width" and "Height" fields are disabled.
I tried few suggestions on stackoverflow and selected the stack view and change the distribution of stack view to "Fill Equally" but still the buttons size is being changed. I dont want this to happen. I want a fixed size buttons in a horizontal stack view and putting them in stack view should not change the size or shape of buttons like this. Can anyone please tell me how do I fix this problem?
Please help.
Sometime Interface Builder is not easy to handle because it is a running layout system at design-time / IB_DESIGNABLE. You make changes, IB gets triggered to 'think', changes parameters, layouts again, you see it does not fit and you change again.
It can be easier to fix UIStackView's constrains to your outer layout before dropping content that will be arranged by taking intrinsicContentSize of the subviews into its calculation. Even worse, if the stackview does not have complete constrains already and you drop something in as being arranged, it will take the default size as intrinsicContentSize of the dropped view and change the stackview spacing as it should. This is no surprise but it can be frustrating as convenience is disturbing your workflow here.
The docs tell you should not change intrinsicContentSize because it is not meant to be animated, it will even disturb animations and layout or even break constrains. Well, you can not set intrinsicContentSize, it is read-only. As thats for good reasons they could have written that while UIView's are instanced they can have supportive variables which have to be set before laying out which allows you to make pre-calculations.
While in code this can be tricky also, you can subclass UIView to make arranged subview instances more supportive to your needs.
There is UIView's invalidateIntrinsicContentSize that triggers the layout to take changed intrinsicContentSize into the next layout cycle. You still cant set intrinsicContentSize, but thats not needed when you would have a class designed like shown below.
// IntrinsicView.h
#import UIKit
IB_DESIGNABLE
#interface IntrinsicView : UIView
-(instancetype)initWithFrame:(CGRect)rect;
#property IBInspectable CGFloat intrinsicHeight;
#property IBInspectable CGFloat intrinsicWidth;
#end
// IntrinsicView.m
#import "IntrinsicView.h"
#implementation IntrinsicView {
CGFloat _intrinsicHeight;
CGFloat _intrinsicWidth;
}
- (instancetype)initWithFrame:(CGRect)frame {
_intrinsicHeight = frame.size.height;
_intrinsicWidth = frame.size.width;
if ( !(self = [super initWithFrame:frame]) ) return nil;
// your stuff here..
return self;
}
-(CGSize)intrinsicContentSize {
return CGSizeMake(_intrinsicWidth, _intrinsicHeight);
}
-(void)prepareForInterfaceBuilder {
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, _intrinsicWidth,_intrinsicHeight);
}
#end
Now this gives you control of the behaviour when UIStackView will layout.
Let's look at instancing of your UIStackView.
#import "IntrinsicView.h"
- (void)viewDidLoad {
[super viewDidLoad];
UIStackView *column = [[UIStackView alloc] initWithFrame:self.view.frame];
column.spacing = 2;
column.alignment = UIStackViewAlignmentFill;
column.axis = UILayoutConstraintAxisVertical; //Up-Down
column.distribution = UIStackViewDistributionFillEqually;
CGFloat quadratur = 30.0;
for (int row=0; row<5; row++) {
IntrinsicView *supportiveView = [[IntrinsicView alloc] initWithFrame:CGRectMake(0, 0, quadratur, quadratur)];
// supportiveView stuff here..
[column addArrangedSubview:supportiveView];
}
[self.view addSubview:column];
}
Don't forget IntrinsicView's intrinsicContentSize is set before instancing is complete, so this example takes frame size at initWithFrame as intended and stores that size to be used when intrinsicContentSize is asked. Having that still needs that UIStackView is large enough to layout nicely but you forced the arranged subviews to that intrinsic size. Btw. the example is arranged up..down.
You can use the IntrinsicView in Interface Builder, just change the views inside UIStackView to the above written class. IB will automatically update the designable API and serve you propertys you can set up. This still needs the StackView to have at least width and height set and also constrains if needed. But it takes away the impression your width and height of arranged views would have any effect other than expected, because IntrinicViews height + width is inactive in IB then.
Just to show you how much this improves your possibilities in IB, see image
I am working on a view which inherits from UIScrollView, and the requirement is that it should start at a contentOffset.y position that is dependent on the view size. Specifically I want to start one screen down in a content that is 3 x the view height.
Like this:
- (void)configureStartCondition {
self.contentSize = CGSizeMake(self.bounds.size.width, self.bounds.size.height * 3.0);
self.contentOffset = CGPointMake(0.0, self.bounds.size.height * 1.0);
}
The view itself is wired up with constraints in Storyboard, just like any view. As it works, the framework will initially give the view the size it has in the storyboard, then when the device size is known, the view's size will be changed to its final size. This is how it should work, and I am fine with this. My question is where do I call configureStartCondition?
An obvious solution would be to put this code in setFrame:, but it doesn't work. setFrame: is only called for the initial frame size, which might or might not be the final size. Why is this?
// NOT working
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self configureStartCondition];
}
A more common place would be in layoutSubview, where I usually do this kind of setup. However, as it is a UIScrollView the layoutSubview is called very frequently as the user scrolls the view. Meaning I would need to save the last height and compare it to make things work, then run through this test millions of times just to be able to initialize. It feels like a kludge to me.
// Working, but ugly
- (void)layoutSubview {
[super layoutSubviews];
if (self.bounds.size.height != self.savedHeight) {
self.savedHeight = self.bounds.size.height;
[self configureStartCondition];
}
// Do layout stuff
}
Another place that may seem good is setBounds:. It will get called for the view size change, but since the contentOffset property is tied to the bounds property, I actually get as many calls here as to layoutSubviews.
So, is there a better place to do it, or a better way to do it?
Side issue, less important in my case, but can the content offset be set from a storyboard?
EDIT: Solutions in Swift are also fine.
setContentSize: works perfect for me. It called only when size changed.
PS: My code to check: (sorry for swift but UIKit make no difference)
class CustomScrollView: UIScrollView {
override var contentSize: CGSize{
didSet{
var offset = contentOffset
offset.y = contentSize.height / 2.0
contentOffset = offset
}
}
}
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
In reading the View Programming Guide from Apple it is apparent they have not updated it for constraints. So a couple of questions:
Many posts here talk about NOT using -initWithFrame when initializing a new UIView or setting it to CGRectZero or some equivalent. How would you handle this then?:
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self){
//Set up rounded rectangle ivar
CGRect frame = [self bounds];
CGFloat radius = frame.size.height / 2;
_rectPath = [UIBezierPath bezierPathWithRoundedRect:frame cornerRadius:radius];
}
}
and then putting the drawing code in the drawRect:. I have found that it won't init properly without a frame. (Obviously, I'm initing this UIView and immediately setting constraints but no frame immediately). This code set's frame = (0,0,0,0) because constraints haven't been computed yet.
If we don't put this code in init where would we put it? Also, I thought it was good practice to init all our stuff we needed in the view in init.
So this leads to question...
When is -updateConstraints called in the runtime cycle and when does the view compute it's bounds using those constraints? I have noticed that the view really doesn't know what it's bounds are until after -layoutSubviews. Is there another place before this that the UIView knows it's bounds if it has only been set with constraints?
Sorry about all the questions, but they all kinda seem connected. Thanks for the looks.
Your problem with your frames is not related to auto layout. View size changes all the time. It is almost never the same as during object instantiation. You need to create/calculate the size of paths to fit the current size during drawRect, not when you first create the view.
The only objects you should set during init are properties and/or ivars that the rest of your class relies on existing in a certain state.
I have an iPad-application. In landscape orientation the UIViewController's view actual width = 1024px and height = 768 - 20 (statusBar) - 44 (navigationBar) = 704px.
So I wanna get this [1024 x 704] size and I'm using self.view.bounds for it. It returns [748 x 1024], which is wrong! But when I rotate the screen twice (current -> portrait -> current), the view's bounds are correct - [1024 x 704].
The view was initialized like this:
- (void)loadView {
self.view = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
self.view.backgroundColor = [UIColor lightGrayColor];
}
And bounds were get like this:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(#"bounds = %#", NSStringFromCGRect(self.view.bounds));
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
NSLog(#"bounds = %#", NSStringFromCGRect(self.view.bounds));
}
So the question is.. How can I get the correct view's bound in the very beginning?
How to do this correctly
Your UIViewController subclass should override the method viewWillLayoutSubviews, see also here.
When this method is called, the viewController's view has its correct size and you can make any necessary adjustments to subviews prior to the layout pass over the subviews.
Swift
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
NSLog("bounds = \(self.view.bounds)")
}
Obj-C
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
NSLog(#"bounds = %#", NSStringFromCGRect(self.view.bounds));
}
Documentation
When a view's bounds change, the view adjusts the position of its subviews. Your view controller can override this method to make changes before the view lays out its subviews. The default implementation of this method does nothing.
As you see from the emphasised part, this method is called every time the view controller's view changes size, as well as when the view first appears. This lets you respond correctly to rotation and other bounds change events
Several ways that don't work
A few approaches suggested in other answers don't work well, or have serious drawbacks. I urge you to avoid these approaches and I'll go through them to discuss the reasons you should avoid them.
viewDidLoad and viewWillAppear – during these calls view does not yet have its final size. The size you get will only ever be correct by pure chance, so as to mislead you.
viewDidAppear – this is too late. Your view is already on screen and visible to the user. Making changes here will cause visible changes / abrupt glitches and will look amateurish. Once again, please – for your sake, for my sake, for everyone's sake: don't do it! You're better than that and so are your users.
UIScreen.mainScreen.bounds.size – this is extremely low level. You're implementing a UIViewController and the size of its view depends on the controllers it is nested in (navigation, tab, paging, any custom controllers, etc), how the device is rotated, and potentially, how the screen has been split up for multitasking. So, while you might be able to compensate for all these and calculate the final size of your view, you'll end up with complex and brittle code that can easily break if Apple decide to change any of these metrics. UIViewController will do all this for you if you just override viewWillLayoutSubviews.
Other than not providing correct information, these problematic approaches will not help you with auto-rotation or other events that cause the view controller's view to change size, such as multitasking gestures. This is something you really want to handle smoothly.
So please: be a champ. Do it the right way. Use viewWillLayoutSubviews. Your implementation will be called for every size change, and your users, future self and team members will celebrate you for it. Bravo!
Further tips
When viewWillLayoutSubviews is called, the only view in your hierarchy that will be resized to its final size is viewController.view. The give away for this is in the name of the method. It's telling you view… (your view controller's root view) …WillLayout… (really soon now, but it's not happened yet) …Subviews (everything else in its hierarchy under the root view).
So subview layout has not happened yet. Every child under the root does not yet have a valid final size. Any size information you query from the child views will be at best completely wrong.
More likely, and enormously worse, it will be misleadingly correct.
It happens to be what you expect and need due to a default at this size and orientation, or due to your storyboard settings. But this is only by chance and isn't something you can rely on with different device sizes or orientations.
If you need to be told when a particular subview changes size, and know its exact final size, you should generally override the layoutSubviews method of that particular UIView subclass.
As per some of the other answers, the issue you are seeing is because viewDidLoad is called before the rotation happens. Because the iPad always initializes in portrait mode, if you get the size values in viewDidLoad, they will always be the portrait sizes - this is irrespective of any orientations you've configured.
To get the size after the orientation/rotation happens, get the size values in viewDidAppear.
I don't particularly understand why iOS doesn't handle this better - especially given that you define the orientations in the project settings, and, in Xcode Interface Builder. But, I'm sure there is a good reason ;-).
I've always used:
CGSize sizeOfScreen = [[UIScreen mainScreen] bounds].size;
to get the size of the screen, and:
CGSize sizeOfView = self.view.bounds.size;
to get the view's size. I have just tested it on viewDidLoad and it returned:
2012-07-17 10:25:46.562 Project[3904:15203] bounds = {768, 1004}
Which is correct since the CGSize is defined as {Width, Height}.
This is what I found in my last project: the frame of self.view will be adjusted after viewDidLoad according to if this screen has navigation bar etc.
So maybe you want to use that value after viewDidLoad (maybe in viewWillAppear or viewDidAppear) or adjust it manually by substract the height of bars.
I ran into this issue and found that getting the bounds in viewDidAppear worked for my needs.
If you want to get correct bounds of view in ViewDidLoad method, you can use Dispatch async with delay for it. See this example below:
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Get your bounds here
}