Everybody knows that you can't trust the frame size on a UIViewController init/viewDidLoad method; this:
- (void)viewDidLoad: {
NSLog(#"%d", self.view.frame.size.width);
}
will print wrong sizes in many occasions (in particular it's pretty much broken in landscape mode)
This will actually return always corrected results so it's good to layout the subviews:
- (void)viewWillAppear: {
NSLog(#"%d", self.view.frame.size.width);
}
The problem is that viewWillAppears gets called every time the view appears, so it's not suitable to alloc or add subviews. So you end up declaring every single view in the interface and you end up with huge header files that I don't like at all since most of the items don't need any more manipulations after the initial setup.
So question one is: Is there a better way to handle subviews positioning?
Question two is very related, let's say I have a subclass of UIView including various others subviews. I declare it inside my interface, and i alloc/init it in my init/viewDidLoad method.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
...
menu = [[SNKSlidingMenu alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
...
}
As we already know we now need to reposition it in viewWillAppear to get a more accurate reading
- (void)viewWillAppear:(BOOL)animated{
....
menu.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
....
}
The problem is that of course all the subviews needs to be repositioned as well. This is done by the layoutSubviews function that get called automatically, but we got the same problem: All the subviews need to be declared inside the interface of the SNKSlidingMenu class.. Is there a way around this?
Thanks.
If you are targetting iOS 5.0 or better you can use viewWillLayoutSubviews and viewDidLayoutSubviews to make changes.
As for your second question, if you need access to an instance variable in other method than init, you need to keep it around, I don't see a problem with it.
You can, however, try to use Auto Layouts and set up rules between the subviews so it's automatically laid out for you without the need to keep a reference.
viewDidLoad only gets called when your view is created, but lots of things can affect the frame's size, and it doesn't get called again when frame changes.
Instead:
create subviews in viewDidLoad
set their sizes in
viewWillLayoutSubviews.
See some additional discussion here for handling rotation: https://stackoverflow.com/a/16421170/1445366
viewWillLayoutSubviews and viewDidLayoutSubviews can resolve this problem.
But the two methed would be performed more times.
this is my code to get correct self.view.frame.
- (void)viewDidLoad {
[super viewDidLoad];
...
dispatch_async(dispatch_get_main_queue(), ^{
// init you view and set it`s frame. this can get correct frame.
...
}
...
}
This saved my life more than once (Swift 4):
override func viewDidLoad() {
super.viewDidLoad()
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
Basically this forces the viewcontroller to correctly layout it's view and from there you can get the correct frames for all your subviews. This particularly helps when doing transition animations and your view controllers are using autolayout and interface builder.
From what I've noticed it looks like the initial frames are set to whatever your interface builder's default size class is set to. I normally edit using the iPhone XS size class so in viewDidLoad it seems that the view's width is always 375 regardless whether you are using an iPhone XR or not. This corrects itself before viewWillAppear though.
The above code will correct this issue and allow you to get the correct frames for your view / subviews before the view controller is rendered to the screen.
Related
In my program I have started doing all initialization of objects in the init method without setting a frame and then in layoutSubviews I set the frames for these objects to make sure that they are properly set.
Firstly is this proper practice to initialize all objects in the init function without a set frame and then in layoutSubviews set each of their frames. The reason for my concern is that it is called quite often.
So I have a UIView subclass where I call these methods in the layoutSubviews
- (void)layoutSubviews
{
[super layoutSubviews];
[self.filterSwitcherView setFrame:self.viewFrame];
[self.drawingView setFrame:self.viewFrame];
[self.textView setFrame:self.textViewFrame];
[self.colorPicker setFrame:self.colorPickerFrame];
}
This currently works fine and all the objects are set correctly, but the problem is in my colorPicker class when the user touches the screen I adjust the frame of the colorPicker and by doing so this method gets called from the subview colorPicker and it readjusts a frame that it shouldn't since it has been modified in the subview. The subview causes the superviews layoutSubview to be called and this is not what I need.
My question is, is there a way to stop this behavior from happening or should I not use layoutSubviews to set frames because I was told this is a better way of making views programmatically?
Off the top of my head, there's two ways to fix this. You can either move this code to where the view is initialized, either in init, initWithFrame:, or initWithCoder:, depending on which you're using. It's good practice to make a separate method to initialize everything for your view, and call it from all the init methods to make sure it's always initialized correctly no matter how you instantiate the view.
Alternatively, if you want to keep your code in layoutSubviews, in your #interface add a boolean to flag that the frames were already set
#interface MyView : UIView
#property (nonatomic, assign) BOOL framesAreSet;
#end
Then when you set your frames, check if you already did
- (void)layoutSubviews
{
[super layoutSubviews];
if (!_framesAreSet)
{
[self.filterSwitcherView setFrame:self.viewFrame];
[self.drawingView setFrame:self.viewFrame];
[self.textView setFrame:self.textViewFrame];
[self.colorPicker setFrame:self.colorPickerFrame];
_framesAreSet = YES;
}
}
Your issue is likely that your colorPicker class is handling the touch methods to adjust its own frame. Instead, you should handle the touch methods in colorPicker's superview class, and have that superview class adjust colorPicker's frame in response to the touches.
Also, I would recommend doing all UI initialization in initWithFrame:, not init. The reason is because calling init on UIView ends up calling initWithFrame:.
Am using xib to design my view using auto layout.Run time I want to change the content size of my scrollview in viewDidLayoutSubviews method but its not working.On orientation change the code written inside viewDidLayoutSubviews works but on load its not working.Please advice what could be the issue?
- (void)viewDidLayoutSubviews
{
scroll.contentSize = CGSizeMake(scroll.frame.size.width, attBtn.frame.origin.y+attBtn.frame.size.height+40);//atnBtn is added programatically
}
Usually when you want to have a view redraw you should call:
[self setNeedsDisplay: YES];
Granted I have never build on iOS, on OSX this code works every time. Also, for example, if you want your delegate to call a redraw for a view named someView:
[someView setNeedsDisplay: YES];
I had this question when/where to create and initialize views that are created programatically, so I hope some discussions here will shed more light on this topic for me.
This slide:
says: "not to initialize something based on the geometry of the view in viewDidLoad" and suggests viewDidAppear.
Imagine my view controller has view. I want to add 10 dynamic UIButtons to it.
Shall I put the code like below to the viewDidAppear?
-(void) viewDidAppear:(BOOL)animated
{
...
UIButton *button1 = [[UIButton alloc] initWithFrame: rect1];
[self.view addSubview: button1];
UIButton *button2 = [[UIButton alloc] initWithFrame: rect2];
[self.view addSubview: button2];
...
}
But this creates the buttons each time the view is shown. Is it what we want?
On the other hand if I put the code in viewDidLoad slide suggest not to initialize geometry of these views there.
Or shall we create buttons in viewDidLoad and set their frames in viewDidAppear?
What approach do you usually take?
But this creates the buttons each time the view is shown. It's true.
So the best thing you can do is to add a boolean (lets name it isLaunched). You set it to FALSE in the method -(void)viewDidLoad
Then add a if condition in your -(void)viewDidAppear where you perform creation of buttons (or other stuff) and set the boolean to true at the end.
You should have something like that :
-(void)viewDidLoad
{
//some settings
isLaunched = FALSE;
}
-(void)viewDidAppear:(BOOL)animated
{
if(!isLaunched)
{
//creating and adding buttons
isLaunched = TRUE;
}
}
zbMax (and now Amar) offered good solutions to implement the view creations in viewDidAppear: I will provide the rational for doing this (over viewDidLoad).
It is pretty simple actually. In viewDidLoad none of the views are actually setup yet, so any attempt to set/create frames or bounds will be extremely inconsistent. Struts and springs (or autolayout) will take effect after this method which will create additional changes to your views. viewDidAppear: is the correct place to do this because you can now rely on existing views and setting frames.
Reason for not playing with the geometry in viewDidLoad is because view is still in the memory and not on the window. Once the view is put on the window, then you can specify geometry. That happens when viewDidAppear is called for your controller.
As recommended, you should do all the initialisation in viewDidLoad as this is one time task and need not be repeated. Hold references to the added subviews and give them appropriate frame in viewDidAppear.
When you are dealing with custom UIView and its subviews, layoutSubviews is the method you need to override in the custom view in order to rearrange the geometry of its subviews.
Hope that helps!
There are a bunch of related questions here, but none that feels like a concise or correct answer. Here's the situation:
I am creating a new ViewController and don't want to use a nib file. My understanding from the Apple docs is that if I don't want to use a nib, I should implement loadView to manually create my own view hierarchy.
However, its not clear to me how I should properly instantiate self.view with the proper bounds (given this view controller might be used in a bunch of different situations, setting it simply to the screen bounds doesn't feel right).
Somehow the default UIViewController loadView does seem to properly initiate the frame size, but its not clear if I'm writing my own version what I should be doing to do this.
There is no need to implement loadView. Instead, implement viewDidLoad and create and add any and all desired subviews you want. Just add them to the default self.view.
If you want to add a view that fills the view controller's view then do something like the following:
- (void)viewDidLoad {
[super viewDidLoad];
UIView *someView = [[UIView alloc] initWithFrame:self.view.bounds];
someView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:someView];
}
In loadView, you should set the view property of viewController, and nothing else. Adding subviews should be done in viewDidLoad.
- (void)loadView {
self.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 80, 40)];
}
Roopesh Chander has an interesting blog post on which strategy to choose: loadView vs viewDidLoad
for Programmatic UI Setup. He recommends setting the frame in loadView rather than viewDidLoad for maximum efficiency.
In my iPad app I have an UITableView. Table's frame size is less than screen size, so to make search functionality look nice I have to adjust searchResultTableView's frame to fit my TableView. I'm doing it in my UISearchDisplayDelegate's -searchDisplayController:willShowSearchResultsTableView: method.
Everything works fine except dimming view. When I'm starting search dimming view's width is equal to screen width:
When I start entering search string or clear textfield my searchResultsTableView resizes properly and everything works as it should:
I tried to change searchResultsTableView frame inside -searchDisplayControllerWillBeginSearch: method using this line
controller.searchResultsTableView.frame = myFrame;
but it doesn't work as well. Any suggestions besides implementing my own search display controller?
I also needed to change the frame of the dimming view but for a different reason. In my case I created a UISearchDisplayController and UISearchBar programmatically in a regular UIViewController not a UITableViewController. I was also using MFSideMenu which added to the complexity of the problem. What ended up happening was the dimming view was in the correct position initially but the next time the search was cleared the dimming view shifted leftwards and upwards by exactly half of it's size. Given the UISearchDisplayController you can find the dimming view like so.
for(UIView * v in controller.searchContentsController.view.subviews)
{
if([v isMemberOfClass:[UIControl class]])
{
v.frame = newFrame; //This frame should account for the UISearchBar
}
}
To handle for the initial incorrect frame you should change it in this delegate method:
- (void) searchDisplayController:(UISearchDisplayController *)controller didShowSearchResultsTableView:(UITableView *)tableView;
To handle for an incorrect frame on any subsequent clears you should change it in this delegate method:
- (void) searchDisplayController:(UISearchDisplayController *)controller didHideSearchResultsTableView:(UITableView *)tableView;
Note: this solution runs through the subviews of the searchContentsController which is one of the reasons I used isMemberOfClass instead of isKindOfClass (UIButton is a subclass of UIControl). Further discrimination would be required if you added a UIControl instance into your view (you could use tags to help determine which ones are yours).