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.
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 testing a sample project with an button . And I write this , but it can't change the button's height .
What's wrong?
class ViewController: UIViewController {
#IBOutlet var test: UIButton!
override func loadView() {
super.loadView()
var newFrame = test.frame
newFrame.size = CGSizeMake(newFrame.width, newFrame.height * 10)
test.frame = newFrame
}
}
And get the same height I set .
Put your code in
viewDidLayoutSubviews
After calling super. Theoretically you need to wait for the auto layout engine to be done before you modify the frame, otherwise it will be set for you. In this case this is probably what happens as on didLoad the autolayout might not be done yet.
Instead of setting the frame, set the constraints for the button. This way you have full control. Especially when you layout other controls along with your button. Makes your life easier. Mostly.
For your reference check the wwdc sessions about auto layout and adaptive ui.
Hope this helps.
I added the MoPub banner ad with the following code:
self.adView.delegate = self
self.adView.frame = CGRectMake(0, self.view.bounds.size.height - MOPUB_BANNER_SIZE.height,
screenWidth, MOPUB_BANNER_SIZE.height)
self.view.addSubview(self.adView)
self.adView.loadAd()
For some reason when I run the application the screen doesn't appear fully, but it has a weird zoom. What could be happening?
UPDATE:
I moved the frame of the adView to viewDidLayoutSubviews() like this:
override func viewDidLoad(){
self.adView.delegate = self
self.view.addSubview(self.adView)
self.adView.loadAd()
}
override func viewDidLayoutSubviews() {
var screenSize: CGRect = UIScreen.mainScreen().bounds
var screenWidth = screenSize.width
self.adView.frame = CGRectMake(0, self.view.bounds.size.height - MOPUB_BANNER_SIZE.height,
MOPUB_BANNER_SIZE.width, MOPUB_BANNER_SIZE.height)
}
But the error persists, I'm using Auto Layout and Size Classes.
The frame properties that are present in viewDidLoad do not reflect the actual frame properties of the view that will be presented on screen. In this method the outlets have been created and all that, but not layed out yet.
Therefore it is the correct place to create an AdView, setting its properties...
BUT the layout has to be done somewhere else - the best place is viewDidLayoutSubviews. If this method is called you can be sure that the frame properties are set correctly.
Before changing anything in your code you can just log the frame in viewDidLoad and log them in viewDidLayoutSubviews - they should be different. Then you can go ahead and move your layout logic over there as well.
I fixed this issue by myself. When working with Spritekit and scenes, remember to always configure the scene before presenting it, specially the scene's ScaleMode. After setting the scene's scaleMode to: scaleMode= .Fill the zoom stopped happening.
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.
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;
}