I have a UICustomButton(subclass of UIButton) in the interface builder. I want change this button's frame inside UICustomButton.
I tried following code:
// make it 10 points wider and higher
CGRect newFrame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width + 10, self.frame.size.height + 10);
self.frame = newFrame;
in awakeFromNib:, drawRect:
none of them worked, the result is unchanged.
Don't do that! That's bad architecture!
A view should never change it's own frame. It's always the parent view resizing its children.
If you detect you need another frame inside your view: send out a delegate call to the parent view and change the frame in there. the delegate method could look like this
- customButton:(UIButton *)button requestNewFrame:(CGRect)frame
{
button.frame = newFrame;
// some storing method so you remember the frame on rotations and stuff
}
Just as a reminder: child views are always resized in
- (void)layoutSubviews //UIView
- (void)viewDidLayoutSubviews //UIViewController
That makes your code safe for different UIInterfaceOrientations and different devices.
Neither awakeFromNib nor drawRect: is appropriate.
You can change its frame in layoutSubviews, but you might run into trouble if your app uses auto layout.
I'd put it in viewDidLoad. If you put it in a recurring method, it will increase in size each time it's called.
Related
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
}
}
}
I am working to migrate my app so I can use it on iPhoneX without scaling. Currently I layout a subview with the following code:
CGRect frame = self.view.bounds;
frame.size.height = 295;
frame.origin.y = 20;
This works fine until you add in the notch. I am not seeing a clear way to find out what the frame.origin.y should be set to, without just hard coding.. Which I would like to avoid.
What I would like to do is this - frame.origin.y = 20 + self.view.safeAreaInserts.top
But that doesn't do a thing.
Thanks!
Perhaps safeAreaInsets is being changed after your code runs (for example if you're trying to perform layout in viewDidLoad). You need to override safeAreaInsetsDidChange to be notified when it changes.
Note that safeAreaInsetsDidChange is a method of UIView, not UIViewController, so you'll need to create and use a subclass of UIView to override the method. It would probably be sufficient to override it like this:
override func safeAreaInsetsDidChange() {
super.safeAreaInsetsDidChange()
setNeedsLayout()
}
And then you can set the subview's frame in layoutSubviews.
Or you could just use constraints instead of setting the frame directly, and let auto layout handle it all for you.
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 have a custom view:
-(id)initWithFrame:(CGRect)frame
{
CGRect frameRect = CGRectMake(0, NAVIGATION_BAR_HEIGHT , frame.size.width, 4 * ROW_HEIGHT + NAVIGATION_BAR_HEIGHT + MESSAGE_BODY_PADDING);
self = [super initWithFrame:frameRect];
if (self) {
_selectionViewWidth = &frame.size.width;
[self initView];
}
return self;
}
-(void)initView
{
CGRect sectionSize = CGRectMake(0, 0 , *(_selectionViewWidth), ROW_HEIGHT * 4);
_selectionView = [[UIView alloc] initWithFrame:sectionSize];
[_selectionView setBackgroundColor:[UIColor clearColor]];
That I use in a View Controller the next way:
_mailAttributesView = [[MailAttributesView alloc]initWithFrame:self.view.frame];
_mailAttributesView.delegate = self;
[self.view addSubview:_mailAttributesView];
So when orientation changes from P to L I have the next problem:
What's the best way to get orientation change callback and redraw my custom view?
You likely need to override your UIView layoutSubviews method and proceed to manually layout your subviews (looks like to/from/cc/subject controls) there.
Or, you could better configure your subview spring/struts (or autolayout constraints) for automatic layout. You could do this in code or via a nib or storyboard.
EDIT: additional info since you seem not to be getting layoutSubviews on orientation change.
My guess is that the viewcontroller-view isn't resizing/repositioning your MailAttributes view either.
It's also not clear when/where you add your MailAttributesView to the veiwcontroller view. If you're doing it in viewDidLoad your viewcontroller view may or may not have a valid frame size (depending if it was loaded from a nib or not). It's best not to depend on the viewcontroller-view frame for layout purposes in viewDidLoad.
Rather, layout any viewcontroller-view subviews in viewWillLayoutSubviews. There your viewcontroller-view frame will be set.
Others may point out that you can set your autoresizingFlags in viewDidLoad for any subviews, but there are gotcha's with this. Primarily if the parent view has zero size, and your subviews are to be inset but have springs/struts defined to glue them to the parent view edges.
The best solution overall IMO is to setup autolayout constraints for everything contained in your viewcontroller view, on down.
In my impression, with autoresizesSubviews = YES, layoutSubviews should be called every time view's size is changed. But I found it is not the case for my view. Is my expectation wrong?
According to sources at Apple,
"-[UIView layoutSubviews] should get called when the size of the view changes."
They also referred me to this, from the the View Programming Guide for iOS:
"Whenever the size of a view changes, UIKit applies the autoresizing behaviors of that view’s subviews and then calls the layoutSubviews method of the view to let it make manual changes. You can implement the layoutSubviews method in custom views when the autoresizing behaviors by themselves do not yield the results you want."
At this point, your best move is to create a small sample project where layoutSubviews does not get called (or, send your existing project) file a bug with Apple using BugReporter, and include that sample project with your bug.
If you need something to happen when your view is resized, you can also override setBounds: and setFrame: for your class to make sure it happens. It would look something like this
-(void)setBounds:(GCRect newBounds) {
// let the UIKit do what it would normally do
[super setBounds:newBounds];
// set the flag to tell UIKit that you'd like your layoutSubviews called
[self setNeedsLayout];
}
-(void)setFrame:(CGRect newFrame) {
// let the UIKit do what it would normally do
[super setFrame:newFrame];
// set the flag to tell UIKit that you'd like your layoutSubviews called
[self setNeedsLayout];
}
The other reason that I sometimes override these methods (temporarily) is so I can stop in the debugger and see when they are getting called and by what code.
From my understanding, layoutSubviews is called when the view's bounds change. This means that if its position changes in its superview (but not its size) then layoutSubviews won't be changed (since the origin point in the bounds is in the view's coordinate system - so it is almost always 0,0). In short, only a change in size will cause this to be fired.
whenever you want to resize the views manually and resizes automatically call layoutSubViews method
-(void)layoutSubviews
{
[super layoutSubviews];
CGRect contentRect = self.contentView.bounds;
CGFloat boundsX = contentRect.origin.x;
CGRect frame,itemlabelframe,statuslabelframe;
frame= CGRectMake(boundsX+1 ,0, 97, 50);
itemlabelframe=CGRectMake(boundsX+100, 0, 155, 50);
statuslabelframe=CGRectMake(boundsX+257, 0, 50, 50);
ItemDescButton.frame=itemlabelframe;
priorityButton.frame = frame;
statusButton.frame=statuslabelframe;
// ItemDescLabel.frame=itemlabelframe;
// statusLabel.frame=statuslabelframe;
}